Coverage for britney2/installability/builder.py: 93%

171 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-04-18 20:48 +0000

1# -*- coding: utf-8 -*- 

2 

3# Copyright (C) 2012 Niels Thykier <niels@thykier.net> 

4 

5# This program is free software; you can redistribute it and/or modify 

6# it under the terms of the GNU General Public License as published by 

7# the Free Software Foundation; either version 2 of the License, or 

8# (at your option) any later version. 

9 

10# This program is distributed in the hope that it will be useful, 

11# but WITHOUT ANY WARRANTY; without even the implied warranty of 

12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

13# GNU General Public License for more details. 

14 

15import apt_pkg 

16from collections import defaultdict 

17from itertools import product 

18 

19from britney2.utils import ifilter_except, iter_except, get_dependency_solvers 

20from britney2.installability.tester import InstallabilityTester 

21from britney2.installability.universe import BinaryPackageRelation, BinaryPackageUniverse 

22 

23 

24def build_installability_tester(suite_info, archs) -> tuple[BinaryPackageUniverse, InstallabilityTester]: 

25 """Create the installability tester""" 

26 

27 builder = InstallabilityTesterBuilder() 

28 

29 for (suite, arch) in product(suite_info, archs): 

30 _build_inst_tester_on_suite_arch(builder, suite_info, suite, arch) 

31 

32 return builder.build() 

33 

34 

35def _build_inst_tester_on_suite_arch(builder, suite_info, suite, arch): 

36 packages_s_a = suite.binaries[arch] 

37 is_target = suite.suite_class.is_target 

38 bin_prov = [(s.binaries[arch], s.provides_table[arch]) for s in suite_info] 

39 solvers = get_dependency_solvers 

40 for pkgdata in packages_s_a.values(): 

41 pkg_id = pkgdata.pkg_id 

42 if not builder.add_binary(pkg_id, 

43 essential=pkgdata.is_essential, 

44 in_testing=is_target): 

45 continue 

46 

47 if pkgdata.conflicts: 

48 conflicts = [] 

49 conflicts_parsed = apt_pkg.parse_depends(pkgdata.conflicts, False) 

50 # Breaks/Conflicts are so simple that we do not need to keep align the relation 

51 # with the suite. This enables us to do a few optimizations. 

52 for dep_binaries_s_a, dep_provides_s_a in bin_prov: 

53 for block in (relation for relation in conflicts_parsed): 

54 # if a package satisfies its own conflicts relation, then it is using §7.6.2 

55 conflicts.extend(s.pkg_id for s in solvers(block, dep_binaries_s_a, dep_provides_s_a) 

56 if s.pkg_id != pkg_id) 

57 else: 

58 conflicts = None 

59 

60 if pkgdata.depends: 

61 depends = _compute_depends(pkgdata, bin_prov, solvers) 

62 else: 

63 depends = None 

64 

65 builder.set_relations(pkg_id, depends, conflicts) 

66 

67 

68def _compute_depends(pkgdata, bin_prov, solvers): 

69 depends = [] 

70 possible_dep_ranges = {} 

71 for block in apt_pkg.parse_depends(pkgdata.depends, False): 

72 sat = {s.pkg_id for binaries_s_a, provides_s_a in bin_prov 

73 for s in solvers(block, binaries_s_a, provides_s_a)} 

74 

75 if len(block) != 1: 

76 depends.append(sat) 

77 else: 

78 # This dependency might be a part 

79 # of a version-range a la: 

80 # 

81 # Depends: pkg-a (>= 1), 

82 # pkg-a (<< 2~) 

83 # 

84 # In such a case we want to reduce 

85 # that to a single clause for 

86 # efficiency. 

87 # 

88 # In theory, it could also happen 

89 # with "non-minimal" dependencies 

90 # a la: 

91 # 

92 # Depends: pkg-a, pkg-a (>= 1) 

93 # 

94 # But dpkg is known to fix that up 

95 # at build time, so we will 

96 # probably only see "ranges" here. 

97 key = block[0][0] 

98 if key in possible_dep_ranges: 

99 possible_dep_ranges[key] &= sat 

100 else: 

101 possible_dep_ranges[key] = sat 

102 

103 if possible_dep_ranges: 

104 depends.extend(possible_dep_ranges.values()) 

105 

106 return depends 

107 

108 

109class InstallabilityTesterBuilder(object): 

110 """Builder to create instances of InstallabilityTester""" 

111 

112 def __init__(self): 

113 self._package_table = {} 

114 self._reverse_package_table = {} 

115 self._essentials = set() 

116 self._testing = set() 

117 self._internmap = {} 

118 self._broken = set() 

119 self._empty_set = self._intern_set(frozenset()) 

120 

121 def add_binary(self, binary, essential=False, in_testing=False, 

122 frozenset=frozenset): 

123 """Add a new binary package 

124 

125 Adds a new binary package. The binary must be given as a 

126 (name, version, architecture)-tuple. Returns True if this 

127 binary is new (i.e. has never been added before) or False 

128 otherwise. 

129 

130 Keyword arguments: 

131 * essential - Whether this package is "Essential: yes". 

132 * in_testing - Whether this package is in testing. 

133 

134 The frozenset argument is a private optimisation. 

135 

136 Cave-at: arch:all packages should be "re-mapped" to given 

137 architecture. That is, (pkg, version, "all") should be 

138 added as: 

139 

140 for arch in architectures: 

141 binary = (pkg, version, arch) 

142 it.add_binary(binary) 

143 

144 The resulting InstallabilityTester relies on this for 

145 correctness! 

146 """ 

147 # Note, even with a dup, we need to do these 

148 if in_testing: 

149 self._testing.add(binary) 

150 if essential: 

151 self._essentials.add(binary) 

152 

153 if binary not in self._package_table: 

154 # Allow binaries to be added multiple times (happens 

155 # when sid and testing have the same version) 

156 self._package_table[binary] = (frozenset(), frozenset()) 

157 return True 

158 return False 

159 

160 def set_relations(self, pkg_id, dependency_clauses, breaks): 

161 """The dependency and breaks relations for a given package 

162 

163 :param pkg_id: BinaryPackageID determining which package will have its relations set 

164 :param dependency_clauses: A list/set of OR clauses (i.e. CNF with each element in 

165 dependency_clauses being a disjunction). Each OR cause (disjunction) should be a 

166 set/list of BinaryPackageIDs that satisfy that relation. 

167 :param breaks: An list/set of BinaryPackageIDs that has a Breaks/Conflicts relation 

168 on the current package. Can be None 

169 :return: No return value 

170 """ 

171 if dependency_clauses is not None: 

172 interned_or_clauses = self._intern_set(self._intern_set(c) for c in dependency_clauses) 

173 satisfiable = True 

174 for or_clause in interned_or_clauses: 

175 if not or_clause: 

176 satisfiable = False 

177 for dep_tuple in or_clause: 

178 rdeps, _, rdep_relations = self._reverse_relations(dep_tuple) 

179 rdeps.add(pkg_id) 

180 rdep_relations.add(or_clause) 

181 

182 if not satisfiable: 

183 self._broken.add(pkg_id) 

184 else: 

185 interned_or_clauses = self._empty_set 

186 

187 if breaks is not None: 

188 # Breaks 

189 breaks_relations = self._intern_set(breaks) 

190 for broken_binary in breaks_relations: 

191 reverse_relations = self._reverse_relations(broken_binary) 

192 reverse_relations[1].add(pkg_id) 

193 else: 

194 breaks_relations = self._empty_set 

195 

196 self._package_table[pkg_id] = (interned_or_clauses, breaks_relations) 

197 

198 def _intern_set(self, s, frozenset=frozenset): 

199 """Freeze and intern a given sequence (set variant of intern()) 

200 

201 Given a sequence, create a frozenset copy (if it is not 

202 already a frozenset) and intern that frozen set. Returns the 

203 interned set. 

204 

205 At first glance, interning sets may seem absurd. However, 

206 it does enable memory savings of up to 600MB when applied 

207 to the "inner" sets of the dependency clauses and all the 

208 conflicts relations as well. 

209 """ 

210 if type(s) is frozenset: 

211 fset = s 

212 else: 

213 fset = frozenset(s) 

214 if fset in self._internmap: 

215 return self._internmap[fset] 

216 self._internmap[fset] = fset 

217 return fset 

218 

219 def _reverse_relations(self, binary, set=set): 

220 """Return the reverse relations for a binary 

221 

222 Fetch the reverse relations for a given binary, which are 

223 created lazily. 

224 """ 

225 

226 if binary in self._reverse_package_table: 

227 return self._reverse_package_table[binary] 

228 rel = [set(), set(), set()] 

229 self._reverse_package_table[binary] = rel 

230 return rel 

231 

232 def build(self) -> tuple[BinaryPackageUniverse, InstallabilityTester]: 

233 """Compile the installability tester 

234 

235 This method will compile an installability tester from the 

236 information given and (where possible) try to optimise a 

237 few things. 

238 """ 

239 package_table = self._package_table 

240 reverse_package_table = self._reverse_package_table 

241 intern_set = self._intern_set 

242 broken = self._broken 

243 not_broken = ifilter_except(broken) 

244 check = set(broken) 

245 

246 # Merge reverse conflicts with conflicts - this saves some 

247 # operations in _check_loop since we only have to check one 

248 # set (instead of two) and we remove a few duplicates here 

249 # and there. 

250 # 

251 # At the same time, intern the rdep sets 

252 for pkg in reverse_package_table: 

253 if pkg not in package_table: # pragma: no cover 

254 raise AssertionError("%s referenced but not added!" % str(pkg)) 

255 deps, con = package_table[pkg] 

256 rdeps, rcon, rdep_relations = reverse_package_table[pkg] 

257 if rcon: 

258 if not con: 

259 con = intern_set(rcon) 

260 else: 

261 con = intern_set(con | rcon) 

262 package_table[pkg] = (deps, con) 

263 reverse_package_table[pkg] = (intern_set(rdeps), con, 

264 intern_set(rdep_relations)) 

265 

266 # Check if we can expand broken. 

267 for t in not_broken(iter_except(check.pop, KeyError)): 267 ↛ 269line 267 didn't jump to line 269, because the loop on line 267 never started

268 # This package is not known to be broken... but it might be now 

269 isb = False 

270 for depgroup in package_table[t][0]: 

271 if not any(not_broken(depgroup)): 

272 # A single clause is unsatisfiable, the 

273 # package can never be installed - add it to 

274 # broken. 

275 isb = True 

276 break 

277 

278 if not isb: 

279 continue 

280 

281 broken.add(t) 

282 

283 if t not in reverse_package_table: 

284 continue 

285 check.update(reverse_package_table[t][0] - broken) 

286 

287 if broken: 

288 # Since a broken package will never be installable, nothing that depends on it 

289 # will ever be installable. Thus, there is no point in keeping relations on 

290 # the broken package. 

291 seen = set() 

292 empty_set = frozenset() 

293 null_data = (frozenset([empty_set]), empty_set) 

294 for b in (x for x in broken if x in reverse_package_table): 

295 for rdep in (r for r in not_broken(reverse_package_table[b][0]) 

296 if r not in seen): 

297 ndep = intern_set((x - broken) for x in package_table[rdep][0]) 

298 package_table[rdep] = (ndep, package_table[rdep][1] - broken) 

299 seen.add(rdep) 

300 

301 # Since they won't affect the installability of any other package, we might as 

302 # as well null their data. This memory for these packages, but likely there 

303 # will only be a handful of these "at best" (fsvo of "best") 

304 for b in broken: 

305 package_table[b] = null_data 

306 if b in reverse_package_table: 

307 del reverse_package_table[b] 

308 

309 relations, eqv_set = self._build_relations_and_eqv_packages_set(package_table, reverse_package_table) 

310 

311 universe = BinaryPackageUniverse(relations, 

312 intern_set(self._essentials), 

313 intern_set(broken), 

314 intern_set(eqv_set)) 

315 

316 solver = InstallabilityTester(universe, self._testing) 

317 

318 return universe, solver 

319 

320 def _build_relations_and_eqv_packages_set(self, 

321 package_table, 

322 reverse_package_table, 

323 frozenset=frozenset): 

324 """Attempt to build a table of equivalent packages 

325 

326 This method attempts to create a table of packages that are 

327 equivalent (in terms of installability). If two packages (A 

328 and B) are equivalent then testing the installability of A is 

329 the same as testing the installability of B. This equivalency 

330 also applies to co-installability. 

331 

332 The example cases: 

333 * aspell-* 

334 * ispell-* 

335 

336 Cases that do *not* apply: 

337 * MTA's 

338 

339 The theory: 

340 

341 The packages A and B are equivalent iff: 

342 

343 reverse_depends(A) == reverse_depends(B) AND 

344 conflicts(A) == conflicts(B) AND 

345 depends(A) == depends(B) 

346 

347 Where "reverse_depends(X)" is the set of reverse dependencies 

348 of X, "conflicts(X)" is the set of negative dependencies of X 

349 (Breaks and Conflicts plus the reverse ones of those combined) 

350 and "depends(X)" is the set of strong dependencies of X 

351 (Depends and Pre-Depends combined). 

352 

353 To be honest, we are actually equally interested another 

354 property as well, namely substitutability. The package A can 

355 always used instead of B, iff: 

356 

357 reverse_depends(A) >= reverse_depends(B) AND 

358 conflicts(A) <= conflicts(B) AND 

359 depends(A) == depends(B) 

360 

361 (With the same definitions as above). Note that equivalency 

362 is just a special-case of substitutability, where A and B can 

363 substitute each other (i.e. a two-way substitution). 

364 

365 Finally, note that the "depends(A) == depends(B)" for 

366 substitutability is actually not a strict requirement. There 

367 are cases where those sets are different without affecting the 

368 property. 

369 """ 

370 # Despite talking about substitutability, the method currently 

371 # only finds the equivalence cases. Lets leave 

372 # substitutability for a future version. 

373 

374 find_eqv_set = defaultdict(list) 

375 eqv_set = set() 

376 relations = {} 

377 emptyset = frozenset() 

378 intern_set = self._intern_set 

379 

380 for pkg in reverse_package_table: 

381 rdeps = reverse_package_table[pkg][2] 

382 if not rdeps: 

383 # we don't care for things without rdeps (because 

384 # it is not worth it) 

385 continue 

386 deps, con = package_table[pkg] 

387 ekey = (deps, con, rdeps) 

388 find_eqv_set[ekey].append(pkg) 

389 

390 for pkg_relations, pkg_list in find_eqv_set.items(): 

391 rdeps = reverse_package_table[pkg_list[0]][0] 

392 rel = BinaryPackageRelation(intern_set(pkg_list), pkg_relations[0], pkg_relations[1], rdeps) 

393 if len(pkg_list) < 2: 

394 relations[pkg_list[0]] = rel 

395 continue 

396 

397 eqv_set.update(pkg_list) 

398 for pkg in pkg_list: 

399 relations[pkg] = rel 

400 

401 for pkg, forward_relations in package_table.items(): 

402 if pkg in relations: 

403 continue 

404 rel = BinaryPackageRelation(intern_set((pkg,)), forward_relations[0], forward_relations[1], emptyset) 

405 relations[pkg] = rel 

406 

407 return relations, eqv_set