Coverage for britney2/britney.py: 83%

770 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-08-18 12:43 +0000

1#!/usr/bin/python3 -u 

2 

3# Copyright (C) 2001-2008 Anthony Towns <ajt@debian.org> 

4# Andreas Barth <aba@debian.org> 

5# Fabio Tranchitella <kobold@debian.org> 

6# Copyright (C) 2010-2013 Adam D. Barratt <adsb@debian.org> 

7 

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

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

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

11# (at your option) any later version. 

12 

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

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

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

16# GNU General Public License for more details. 

17 

18""" 

19= Introduction = 

20 

21This is the Debian testing updater script, also known as "Britney". 

22 

23Packages are usually installed into the `testing' distribution after 

24they have undergone some degree of testing in unstable. The goal of 

25this software is to do this task in a smart way, allowing testing 

26to always be fully installable and close to being a release candidate. 

27 

28Britney's source code is split between two different but related tasks: 

29the first one is the generation of the update excuses, while the 

30second tries to update testing with the valid candidates; first 

31each package alone, then larger and even larger sets of packages 

32together. Each try is accepted if testing is not more uninstallable 

33after the update than before. 

34 

35= Data Loading = 

36 

37In order to analyze the entire Debian distribution, Britney needs to 

38load in memory the whole archive: this means more than 10.000 packages 

39for twelve architectures, as well as the dependency interconnections 

40between them. For this reason, the memory requirements for running this 

41software are quite high and at least 1 gigabyte of RAM should be available. 

42 

43Britney loads the source packages from the `Sources' file and the binary 

44packages from the `Packages_${arch}' files, where ${arch} is substituted 

45with the supported architectures. While loading the data, the software 

46analyzes the dependencies and builds a directed weighted graph in memory 

47with all the interconnections between the packages (see Britney.read_sources 

48and Britney.read_binaries). 

49 

50Other than source and binary packages, Britney loads the following data: 

51 

52 * rc-bugs-*, which contains the list of release-critical bugs for a given 

53 version of a source or binary package (see RCBugPolicy.read_bugs). 

54 

55 * age-policy-dates, which contains the date of the upload of a given version 

56 of a source package (see Britney.read_dates). 

57 

58 * age-policy-urgencies, which contains the urgency of the upload of a given 

59 version of a source package (see AgePolicy._read_urgencies). 

60 

61 * Hints, which contains lists of commands which modify the standard behaviour 

62 of Britney (see Britney.read_hints). 

63 

64 * Other policies typically require their own data. 

65 

66For a more detailed explanation about the format of these files, please read 

67the documentation of the related methods. The exact meaning of them will be 

68instead explained in the chapter "Excuses Generation". 

69 

70= Excuses = 

71 

72An excuse is a detailed explanation of why a package can or cannot 

73be updated in the testing distribution from a newer package in 

74another distribution (like for example unstable). The main purpose 

75of the excuses is to be written in an HTML file which will be 

76published over HTTP, as well as a YAML file. The maintainers will be able 

77to parse it manually or automatically to find the explanation of why their 

78packages have been updated or not. 

79 

80== Excuses generation == 

81 

82These are the steps (with references to method names) that Britney 

83does for the generation of the update excuses. 

84 

85 * If a source package is available in testing but it is not 

86 present in unstable and no binary packages in unstable are 

87 built from it, then it is marked for removal. 

88 

89 * Every source package in unstable and testing-proposed-updates, 

90 if already present in testing, is checked for binary-NMUs, new 

91 or dropped binary packages in all the supported architectures 

92 (see Britney.should_upgrade_srcarch). The steps to detect if an 

93 upgrade is needed are: 

94 

95 1. If there is a `remove' hint for the source package, the package 

96 is ignored: it will be removed and not updated. 

97 

98 2. For every binary package built from the new source, it checks 

99 for unsatisfied dependencies, new binary packages and updated 

100 binary packages (binNMU), excluding the architecture-independent 

101 ones, and packages not built from the same source. 

102 

103 3. For every binary package built from the old source, it checks 

104 if it is still built from the new source; if this is not true 

105 and the package is not architecture-independent, the script 

106 removes it from testing. 

107 

108 4. Finally, if there is something worth doing (eg. a new or updated 

109 binary package) and nothing wrong it marks the source package 

110 as "Valid candidate", or "Not considered" if there is something 

111 wrong which prevented the update. 

112 

113 * Every source package in unstable and testing-proposed-updates is 

114 checked for upgrade (see Britney.should_upgrade_src). The steps 

115 to detect if an upgrade is needed are: 

116 

117 1. If the source package in testing is more recent the new one 

118 is ignored. 

119 

120 2. If the source package doesn't exist (is fake), which means that 

121 a binary package refers to it but it is not present in the 

122 `Sources' file, the new one is ignored. 

123 

124 3. If the package doesn't exist in testing, the urgency of the 

125 upload is ignored and set to the default (actually `low'). 

126 

127 4. If there is a `remove' hint for the source package, the package 

128 is ignored: it will be removed and not updated. 

129 

130 5. If there is a `block' hint for the source package without an 

131 `unblock` hint or a `block-all source`, the package is ignored. 

132 

133 6. If there is a `block-udeb' hint for the source package, it will 

134 have the same effect as `block', but may only be cancelled by 

135 a subsequent `unblock-udeb' hint. 

136 

137 7. If the suite is unstable, the update can go ahead only if the 

138 upload happened more than the minimum days specified by the 

139 urgency of the upload; if this is not true, the package is 

140 ignored as `too-young'. Note that the urgency is sticky, meaning 

141 that the highest urgency uploaded since the previous testing 

142 transition is taken into account. 

143 

144 8. If the suite is unstable, all the architecture-dependent binary 

145 packages and the architecture-independent ones for the `nobreakall' 

146 architectures have to be built from the source we are considering. 

147 If this is not true, then these are called `out-of-date' 

148 architectures and the package is ignored. 

149 

150 9. The source package must have at least one binary package, otherwise 

151 it is ignored. 

152 

153 10. If the suite is unstable, the new source package must have no 

154 release critical bugs which do not also apply to the testing 

155 one. If this is not true, the package is ignored as `buggy'. 

156 

157 11. If there is a `force' hint for the source package, then it is 

158 updated even if it is marked as ignored from the previous steps. 

159 

160 12. If the suite is {testing-,}proposed-updates, the source package can 

161 be updated only if there is an explicit approval for it. Unless 

162 a `force' hint exists, the new package must also be available 

163 on all of the architectures for which it has binary packages in 

164 testing. 

165 

166 13. If the package will be ignored, mark it as "Valid candidate", 

167 otherwise mark it as "Not considered". 

168 

169 * The list of `remove' hints is processed: if the requested source 

170 package is not already being updated or removed and the version 

171 actually in testing is the same specified with the `remove' hint, 

172 it is marked for removal. 

173 

174 * The excuses are sorted by the number of days from the last upload 

175 (days-old) and by name. 

176 

177 * A list of unconsidered excuses (for which the package is not upgraded) 

178 is built. Using this list, all of the excuses depending on them are 

179 marked as invalid "impossible dependencies". 

180 

181 * The excuses are written in an HTML file. 

182""" 

183import contextlib 

184import logging 

185import optparse 

186import os 

187import sys 

188import time 

189from collections import defaultdict 

190from collections.abc import Generator 

191from functools import reduce 

192from itertools import chain 

193from operator import attrgetter 

194from typing import TYPE_CHECKING, Any, Optional 

195 

196import apt_pkg 

197 

198from britney2 import BinaryPackage, BinaryPackageId, MultiArch, SourcePackage, Suites 

199from britney2.excusefinder import ExcuseFinder 

200from britney2.hints import Hint, HintCollection, HintParser 

201from britney2.inputs.suiteloader import ( 

202 DebMirrorLikeSuiteContentLoader, 

203 MissingRequiredConfigurationError, 

204) 

205from britney2.installability.builder import build_installability_tester 

206from britney2.installability.solver import InstallabilitySolver 

207from britney2.migration import MigrationManager 

208from britney2.migrationitem import MigrationItem, MigrationItemFactory 

209from britney2.policies.autopkgtest import AutopkgtestPolicy 

210from britney2.policies.lintian import LintianPolicy 

211from britney2.policies.policy import ( 

212 AgePolicy, 

213 BlockPolicy, 

214 BuildDependsPolicy, 

215 BuiltOnBuilddPolicy, 

216 BuiltUsingPolicy, 

217 DependsPolicy, 

218 ImplicitDependencyPolicy, 

219 PiupartsPolicy, 

220 PolicyEngine, 

221 PolicyLoadRequest, 

222 RCBugPolicy, 

223 ReproduciblePolicy, 

224 ReverseRemovalPolicy, 

225) 

226from britney2.utils import ( 

227 ExcusesOutputFormat, 

228 MigrationConstraintException, 

229 clone_nuninst, 

230 compile_nuninst, 

231 format_and_log_uninst, 

232 is_nuninst_asgood_generous, 

233 log_and_format_old_libraries, 

234 newly_uninst, 

235 old_libraries, 

236 parse_option, 

237 parse_provides, 

238 read_nuninst, 

239 write_excuses, 

240 write_heidi, 

241 write_heidi_delta, 

242 write_nuninst, 

243) 

244 

245if TYPE_CHECKING: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 from .excuse import Excuse 

247 from .installability.tester import InstallabilityTester 

248 from .installability.universe import BinaryPackageUniverse 

249 from .transaction import MigrationTransactionState 

250 

251 

252__author__ = "Fabio Tranchitella and the Debian Release Team" 

253__version__ = "2.0" 

254 

255 

256MIGRATION_POLICIES = [ 

257 PolicyLoadRequest.always_load(DependsPolicy), 

258 PolicyLoadRequest.conditionally_load(RCBugPolicy, "rcbug_enable", True), 

259 PolicyLoadRequest.conditionally_load(PiupartsPolicy, "piuparts_enable", True), 

260 PolicyLoadRequest.always_load(ImplicitDependencyPolicy), 

261 PolicyLoadRequest.conditionally_load(AutopkgtestPolicy, "adt_enable", True), 

262 PolicyLoadRequest.conditionally_load(LintianPolicy, "lintian_enable", False), 

263 PolicyLoadRequest.conditionally_load(ReproduciblePolicy, "repro_enable", False), 

264 PolicyLoadRequest.conditionally_load(AgePolicy, "age_enable", True), 

265 PolicyLoadRequest.always_load(BuildDependsPolicy), 

266 PolicyLoadRequest.always_load(BlockPolicy), 

267 PolicyLoadRequest.conditionally_load( 

268 BuiltUsingPolicy, "built_using_policy_enable", True 

269 ), 

270 PolicyLoadRequest.conditionally_load(BuiltOnBuilddPolicy, "check_buildd", False), 

271 PolicyLoadRequest.always_load(ReverseRemovalPolicy), 

272] 

273 

274 

275class Britney: 

276 """Britney, the Debian testing updater script 

277 

278 This is the script that updates the testing distribution. It is executed 

279 each day after the installation of the updated packages. It generates the 

280 `Packages' files for the testing distribution, but it does so in an 

281 intelligent manner; it tries to avoid any inconsistency and to use only 

282 non-buggy packages. 

283 

284 For more documentation on this script, please read the Developers Reference. 

285 """ 

286 

287 HINTS_HELPERS = ( 

288 "easy", 

289 "hint", 

290 "remove", 

291 "block", 

292 "block-udeb", 

293 "unblock", 

294 "unblock-udeb", 

295 "approve", 

296 "remark", 

297 "ignore-lintian", 

298 "ignore-piuparts", 

299 "ignore-rc-bugs", 

300 "ignore-reproducible", 

301 "ignore-reproducible-src", 

302 "force-skiptest", 

303 "force-badtest", 

304 ) 

305 HINTS_STANDARD = ("urgent", "age-days") + HINTS_HELPERS 

306 # ALL = {"force", "force-hint", "block-all"} | HINTS_STANDARD | registered policy hints (not covered above) 

307 HINTS_ALL = "ALL" 

308 pkg_universe: "BinaryPackageUniverse" 

309 _inst_tester: "InstallabilityTester" 

310 constraints: dict[str, list[str]] 

311 suite_info: Suites 

312 

313 def __init__(self) -> None: 

314 """Class constructor 

315 

316 This method initializes and populates the data lists, which contain all 

317 the information needed by the other methods of the class. 

318 """ 

319 

320 # setup logging - provide the "short level name" (i.e. INFO -> I) that 

321 # we used to use prior to using the logging module. 

322 

323 old_factory = logging.getLogRecordFactory() 

324 short_level_mapping = { 

325 "CRITICAL": "F", 

326 "INFO": "I", 

327 "WARNING": "W", 

328 "ERROR": "E", 

329 "DEBUG": "N", 

330 } 

331 

332 def record_factory( 

333 *args: Any, **kwargs: Any 

334 ) -> logging.LogRecord: # pragma: no cover 

335 record = old_factory(*args, **kwargs) 

336 try: 

337 record.shortlevelname = short_level_mapping[record.levelname] 

338 except KeyError: 

339 record.shortlevelname = record.levelname 

340 return record 

341 

342 logging.setLogRecordFactory(record_factory) 

343 logging.basicConfig( 

344 format="{shortlevelname}: [{asctime}] - {message}", 

345 style="{", 

346 datefmt="%Y-%m-%dT%H:%M:%S%z", 

347 stream=sys.stdout, 

348 ) 

349 

350 self.logger = logging.getLogger() 

351 

352 # Logger for "upgrade_output"; the file handler will be attached later when 

353 # we are ready to open the file. 

354 self.output_logger = logging.getLogger("britney2.output.upgrade_output") 

355 self.output_logger.setLevel(logging.INFO) 

356 

357 # initialize the apt_pkg back-end 

358 apt_pkg.init() 

359 

360 # parse the command line arguments 

361 self._policy_engine = PolicyEngine() 

362 self.__parse_arguments() 

363 assert self.suite_info is not None # for type checking 

364 

365 self.all_selected: list[MigrationItem] = [] 

366 self.excuses: dict[str, "Excuse"] = {} 

367 self.upgrade_me: list[MigrationItem] = [] 

368 

369 if self.options.nuninst_cache: 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true

370 self.logger.info( 

371 "Not building the list of non-installable packages, as requested" 

372 ) 

373 if self.options.print_uninst: 

374 nuninst = read_nuninst( 

375 self.options.noninst_status, self.options.architectures 

376 ) 

377 print("* summary") 

378 print( 

379 "\n".join( 

380 "%4d %s" % (len(nuninst[x]), x) 

381 for x in self.options.architectures 

382 ) 

383 ) 

384 return 

385 

386 try: 

387 constraints_file = os.path.join( 

388 self.options.static_input_dir, "constraints" 

389 ) 

390 faux_packages = os.path.join(self.options.static_input_dir, "faux-packages") 

391 except AttributeError: 

392 self.logger.info("The static_input_dir option is not set") 

393 constraints_file = None 

394 faux_packages = None 

395 if faux_packages is not None and os.path.exists(faux_packages): 

396 self.logger.info("Loading faux packages from %s", faux_packages) 

397 self._load_faux_packages(faux_packages) 

398 elif faux_packages is not None: 398 ↛ 401line 398 didn't jump to line 401 because the condition on line 398 was always true

399 self.logger.info("No Faux packages as %s does not exist", faux_packages) 

400 

401 if constraints_file is not None and os.path.exists(constraints_file): 

402 self.logger.info("Loading constraints from %s", constraints_file) 

403 self.constraints = self._load_constraints(constraints_file) 

404 else: 

405 if constraints_file is not None: 405 ↛ 409line 405 didn't jump to line 409

406 self.logger.info( 

407 "No constraints as %s does not exist", constraints_file 

408 ) 

409 self.constraints = { 

410 "keep-installable": [], 

411 } 

412 

413 self.logger.info("Compiling Installability tester") 

414 self.pkg_universe, self._inst_tester = build_installability_tester( 

415 self.suite_info, self.options.architectures 

416 ) 

417 target_suite = self.suite_info.target_suite 

418 target_suite.inst_tester = self._inst_tester 

419 

420 self.allow_uninst: dict[str, set[str | None]] = {} 

421 for arch in self.options.architectures: 

422 self.allow_uninst[arch] = set() 

423 self._migration_item_factory: MigrationItemFactory = MigrationItemFactory( 

424 self.suite_info 

425 ) 

426 self._hint_parser: HintParser = HintParser(self._migration_item_factory) 

427 self._migration_manager: MigrationManager = MigrationManager( 

428 self.options, 

429 self.suite_info, 

430 self.all_binaries, 

431 self.pkg_universe, 

432 self.constraints, 

433 self.allow_uninst, 

434 self._migration_item_factory, 

435 self.hints, 

436 ) 

437 

438 if not self.options.nuninst_cache: 438 ↛ 478line 438 didn't jump to line 478 because the condition on line 438 was always true

439 self.logger.info( 

440 "Building the list of non-installable packages for the full archive" 

441 ) 

442 self._inst_tester.compute_installability() 

443 nuninst = compile_nuninst( 

444 target_suite, self.options.architectures, self.options.nobreakall_arches 

445 ) 

446 self.nuninst_orig: dict[str, set[str]] = nuninst 

447 for arch in self.options.architectures: 

448 self.logger.info( 

449 "> Found %d non-installable packages for %s", 

450 len(nuninst[arch]), 

451 arch, 

452 ) 

453 if self.options.print_uninst: 453 ↛ 454line 453 didn't jump to line 454 because the condition on line 453 was never true

454 self.nuninst_arch_report(nuninst, arch) 

455 

456 if self.options.print_uninst: 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true

457 print("* summary") 

458 print( 

459 "\n".join( 

460 map( 

461 lambda x: "%4d %s" % (len(nuninst[x]), x), 

462 self.options.architectures, 

463 ) 

464 ) 

465 ) 

466 return 

467 else: 

468 write_nuninst(self.options.noninst_status, nuninst) 

469 

470 stats = self._inst_tester.compute_stats() 

471 self.logger.info("> Installability tester statistics (per architecture)") 

472 for arch in self.options.architectures: 

473 arch_stat = stats[arch] 

474 self.logger.info("> %s", arch) 

475 for stat in arch_stat.stat_summary(): 

476 self.logger.info("> - %s", stat) 

477 else: 

478 self.logger.info("Loading uninstallability counters from cache") 

479 self.nuninst_orig = read_nuninst( 

480 self.options.noninst_status, self.options.architectures 

481 ) 

482 

483 # nuninst_orig may get updated during the upgrade process 

484 self.nuninst_orig_save: dict[str, set[str]] = clone_nuninst( 

485 self.nuninst_orig, architectures=self.options.architectures 

486 ) 

487 

488 self._policy_engine.register_policy_hints(self._hint_parser) 

489 

490 try: 

491 self.read_hints(self.options.hintsdir) 

492 except AttributeError: 

493 self.read_hints(os.path.join(self.suite_info["unstable"].path, "Hints")) 

494 

495 self._policy_engine.initialise(self, self.hints) 

496 

497 def __parse_arguments(self) -> None: 

498 """Parse the command line arguments 

499 

500 This method parses and initializes the command line arguments. 

501 While doing so, it preprocesses some of the options to be converted 

502 in a suitable form for the other methods of the class. 

503 """ 

504 # initialize the parser 

505 parser = optparse.OptionParser(version="%prog") 

506 parser.add_option( 

507 "-v", "", action="count", dest="verbose", help="enable verbose output" 

508 ) 

509 parser.add_option( 

510 "-c", 

511 "--config", 

512 action="store", 

513 dest="config", 

514 default="/etc/britney.conf", 

515 help="path for the configuration file", 

516 ) 

517 parser.add_option( 

518 "", 

519 "--architectures", 

520 action="store", 

521 dest="architectures", 

522 default=None, 

523 help="override architectures from configuration file", 

524 ) 

525 parser.add_option( 

526 "", 

527 "--actions", 

528 action="store", 

529 dest="actions", 

530 default=None, 

531 help="override the list of actions to be performed", 

532 ) 

533 parser.add_option( 

534 "", 

535 "--hints", 

536 action="store", 

537 dest="hints", 

538 default=None, 

539 help="additional hints, separated by semicolons", 

540 ) 

541 parser.add_option( 

542 "", 

543 "--hint-tester", 

544 action="store_true", 

545 dest="hint_tester", 

546 default=None, 

547 help="provide a command line interface to test hints", 

548 ) 

549 parser.add_option( 

550 "", 

551 "--dry-run", 

552 action="store_true", 

553 dest="dry_run", 

554 default=False, 

555 help="disable all outputs to the testing directory", 

556 ) 

557 parser.add_option( 

558 "", 

559 "--nuninst-cache", 

560 action="store_true", 

561 dest="nuninst_cache", 

562 default=False, 

563 help="do not build the non-installability status, use the cache from file", 

564 ) 

565 parser.add_option( 

566 "", 

567 "--print-uninst", 

568 action="store_true", 

569 dest="print_uninst", 

570 default=False, 

571 help="just print a summary of uninstallable packages", 

572 ) 

573 parser.add_option( 

574 "", 

575 "--compute-migrations", 

576 action="store_true", 

577 dest="compute_migrations", 

578 default=True, 

579 help="Compute which packages can migrate (the default)", 

580 ) 

581 parser.add_option( 

582 "", 

583 "--no-compute-migrations", 

584 action="store_false", 

585 dest="compute_migrations", 

586 help="Do not compute which packages can migrate.", 

587 ) 

588 parser.add_option( 

589 "", 

590 "--series", 

591 action="store", 

592 dest="series", 

593 default="", 

594 help="set distribution series name", 

595 ) 

596 parser.add_option( 

597 "", 

598 "--distribution", 

599 action="store", 

600 dest="distribution", 

601 default="debian", 

602 help="set distribution name", 

603 ) 

604 (self.options, self.args) = parser.parse_args() 

605 

606 if self.options.verbose: 606 ↛ 612line 606 didn't jump to line 612 because the condition on line 606 was always true

607 if self.options.verbose > 1: 607 ↛ 608line 607 didn't jump to line 608 because the condition on line 607 was never true

608 self.logger.setLevel(logging.DEBUG) 

609 else: 

610 self.logger.setLevel(logging.INFO) 

611 else: 

612 self.logger.setLevel(logging.WARNING) 

613 # Historical way to get debug information (equivalent to -vv) 

614 try: # pragma: no cover 

615 if int(os.environ.get("BRITNEY_DEBUG", "0")): 

616 self.logger.setLevel(logging.DEBUG) 

617 except ValueError: # pragma: no cover 

618 pass 

619 

620 # integrity checks 

621 if self.options.nuninst_cache and self.options.print_uninst: # pragma: no cover 

622 self.logger.error("nuninst_cache and print_uninst are mutually exclusive!") 

623 sys.exit(1) 

624 

625 # if the configuration file exists, then read it and set the additional options 

626 if not os.path.isfile(self.options.config): # pragma: no cover 

627 self.logger.error( 

628 "Unable to read the configuration file (%s), exiting!", 

629 self.options.config, 

630 ) 

631 sys.exit(1) 

632 

633 self.HINTS: dict[str, Any] = {"command-line": self.HINTS_ALL} 

634 with open(self.options.config, encoding="utf-8") as config: 

635 for line in config: 

636 if "=" in line and not line.strip().startswith("#"): 

637 k, v = line.split("=", 1) 

638 k = k.strip() 

639 v = v.strip() 

640 if k.startswith("HINTS_"): 

641 self.HINTS[k.split("_")[1].lower()] = reduce( 641 ↛ exitline 641 didn't jump to the function exit

642 lambda x, y: x + y, 

643 [ 

644 hasattr(self, "HINTS_" + i) 

645 and getattr(self, "HINTS_" + i) 

646 or (i,) 

647 for i in v.split() 

648 ], 

649 ) 

650 elif not hasattr(self.options, k.lower()) or not getattr( 

651 self.options, k.lower() 

652 ): 

653 setattr(self.options, k.lower(), v) 

654 

655 parse_option(self.options, "archall_inconsistency_allowed", to_bool=True) 

656 parse_option( 

657 self.options, "be_strict_with_build_deps", default=True, to_bool=True 

658 ) 

659 

660 suite_loader = DebMirrorLikeSuiteContentLoader(self.options) 

661 

662 try: 

663 self.suite_info = suite_loader.load_suites() 

664 except MissingRequiredConfigurationError as e: # pragma: no cover 

665 self.logger.error( 

666 "Could not load the suite content due to missing configuration: %s", 

667 e, 

668 ) 

669 sys.exit(1) 

670 self.all_binaries = suite_loader.all_binaries() 

671 self.options.components = suite_loader.components 

672 self.options.architectures = suite_loader.architectures 

673 self.options.nobreakall_arches = suite_loader.nobreakall_arches 

674 self.options.outofsync_arches = suite_loader.outofsync_arches 

675 self.options.break_arches = suite_loader.break_arches 

676 self.options.new_arches = suite_loader.new_arches 

677 if self.options.series == "": 677 ↛ 680line 677 didn't jump to line 680 because the condition on line 677 was always true

678 self.options.series = self.suite_info.target_suite.name 

679 

680 if self.options.heidi_output and not hasattr( 680 ↛ 685line 680 didn't jump to line 685 because the condition on line 680 was always true

681 self.options, "heidi_delta_output" 

682 ): 

683 self.options.heidi_delta_output = self.options.heidi_output + "Delta" 

684 

685 self.options.smooth_updates = self.options.smooth_updates.split() 

686 

687 parse_option(self.options, "ignore_cruft", to_bool=True) 

688 parse_option(self.options, "check_consistency_level", default=2, to_int=True) 

689 parse_option(self.options, "build_url") 

690 

691 self._policy_engine.load_policies( 

692 self.options, self.suite_info, MIGRATION_POLICIES 

693 ) 

694 

695 @property 

696 def hints(self) -> HintCollection: 

697 return self._hint_parser.hints 

698 

699 def _load_faux_packages(self, faux_packages_file: str) -> None: 

700 """Loads fake packages 

701 

702 In rare cases, it is useful to create a "fake" package that can be used to satisfy 

703 dependencies. This is usually needed for packages that are not shipped directly 

704 on this mirror but is a prerequisite for using this mirror (e.g. some vendors provide 

705 non-distributable "setup" packages and contrib/non-free packages depend on these). 

706 

707 :param faux_packages_file: Path to the file containing the fake package definitions 

708 """ 

709 tag_file = apt_pkg.TagFile(faux_packages_file) 

710 get_field = tag_file.section.get 

711 step = tag_file.step 

712 no = 0 

713 pri_source_suite = self.suite_info.primary_source_suite 

714 target_suite = self.suite_info.target_suite 

715 

716 while step(): 

717 no += 1 

718 pkg_name = get_field("Package", None) 

719 if pkg_name is None: # pragma: no cover 

720 raise ValueError( 

721 f"Missing Package field in paragraph {no} (file {faux_packages_file})" 

722 ) 

723 version = get_field("Version", "1.0-1") 

724 provides_raw = get_field("Provides") 

725 archs_raw = get_field("Architecture", None) 

726 component = get_field("Component", "non-free") 

727 if archs_raw: 727 ↛ 728line 727 didn't jump to line 728 because the condition on line 727 was never true

728 archs = archs_raw.split() 

729 else: 

730 archs = self.options.architectures 

731 faux_section = "faux" 

732 if component != "main": 732 ↛ 734line 732 didn't jump to line 734 because the condition on line 732 was always true

733 faux_section = f"{component}/faux" 

734 src_data = SourcePackage( 

735 pkg_name, 

736 version, 

737 faux_section, 

738 set(), 

739 None, 

740 True, 

741 ) 

742 

743 target_suite.sources[pkg_name] = src_data 

744 pri_source_suite.sources[pkg_name] = src_data 

745 

746 for arch in archs: 

747 pkg_id = BinaryPackageId(pkg_name, version, arch) 

748 if provides_raw: 748 ↛ 749line 748 didn't jump to line 749 because the condition on line 748 was never true

749 provides = parse_provides( 

750 provides_raw, pkg_id=pkg_id, logger=self.logger 

751 ) 

752 else: 

753 provides = None 

754 bin_data = BinaryPackage( 

755 faux_section, 

756 pkg_name, 

757 version, 

758 arch, 

759 MultiArch.from_str(get_field("Multi-Arch")), 

760 None, 

761 None, 

762 provides, 

763 False, 

764 pkg_id, 

765 None, 

766 ) 

767 

768 src_data.binaries.add(pkg_id) 

769 target_suite.binaries[arch][pkg_name] = bin_data 

770 pri_source_suite.binaries[arch][pkg_name] = bin_data 

771 

772 # register provided packages with the target suite provides table 

773 for provided_pkg, provided_version in ( 773 ↛ 776line 773 didn't jump to line 776

774 bin_data.provides if bin_data.provides is not None else [] 

775 ): 

776 target_suite.provides_table[arch][provided_pkg].add( 

777 (pkg_name, provided_version) 

778 ) 

779 

780 self.all_binaries[pkg_id] = bin_data 

781 

782 def _load_constraints(self, constraints_file: str) -> dict[str, list[str]]: 

783 """Loads configurable constraints 

784 

785 The constraints file can contain extra rules that Britney should attempt 

786 to satisfy. Examples can be "keep package X in testing and ensure it is 

787 installable". 

788 

789 :param constraints_file: Path to the file containing the constraints 

790 """ 

791 tag_file = apt_pkg.TagFile(constraints_file) 

792 get_field = tag_file.section.get 

793 step = tag_file.step 

794 no = 0 

795 faux_version = "1" 

796 faux_section = "faux" 

797 keep_installable: list[str] = [] 

798 constraints = {"keep-installable": keep_installable} 

799 pri_source_suite = self.suite_info.primary_source_suite 

800 target_suite = self.suite_info.target_suite 

801 

802 while step(): 

803 no += 1 

804 pkg_name = get_field("Fake-Package-Name", None) 

805 if pkg_name is None: # pragma: no cover 

806 raise ValueError( 

807 f"Missing Fake-Package-Name field in paragraph {no} (file {constraints_file})" 

808 ) 

809 

810 def mandatory_field(x: str) -> str: 

811 v: str = get_field(x, None) 

812 if v is None: # pragma: no cover 

813 raise ValueError( 

814 f"Missing {x} field for {pkg_name} (file {constraints_file})" 

815 ) 

816 return v 

817 

818 constraint = mandatory_field("Constraint") 

819 if constraint not in {"present-and-installable"}: # pragma: no cover 

820 raise ValueError( 

821 f"Unsupported constraint {constraint} for {pkg_name} (file {constraints_file})" 

822 ) 

823 

824 self.logger.info(" - constraint %s", pkg_name) 

825 

826 pkg_list = [ 

827 x.strip() 

828 for x in mandatory_field("Package-List").split("\n") 

829 if x.strip() != "" and not x.strip().startswith("#") 

830 ] 

831 src_data = SourcePackage( 

832 pkg_name, 

833 faux_version, 

834 faux_section, 

835 set(), 

836 None, 

837 True, 

838 ) 

839 target_suite.sources[pkg_name] = src_data 

840 pri_source_suite.sources[pkg_name] = src_data 

841 keep_installable.append(pkg_name) 

842 for arch in self.options.architectures: 

843 deps = [] 

844 for pkg_spec in pkg_list: 

845 s = pkg_spec.split(None, 1) 

846 if len(s) == 1: 

847 deps.append(s[0]) 

848 else: 

849 pkg, arch_res = s 

850 if not ( 

851 arch_res.startswith("[") and arch_res.endswith("]") 

852 ): # pragma: no cover 

853 raise ValueError( 

854 f"Invalid arch-restriction on {pkg} - should be [arch1 arch2] " 

855 f"(for {pkg_name} file {constraints_file})" 

856 ) 

857 arch_res_l = arch_res[1:-1].split() 

858 if not arch_res_l: # pragma: no cover 

859 raise ValueError( 

860 f"Empty arch-restriction for {pkg}: Uses comma or negation " 

861 f"(for {pkg_name} file {constraints_file})" 

862 ) 

863 for a in arch_res_l: 

864 if a == arch: 

865 deps.append(pkg) 

866 elif "," in a or "!" in a: # pragma: no cover 

867 raise ValueError( 

868 f"Invalid arch-restriction for {pkg}: Uses comma or negation " 

869 f"(for {pkg_name} file {constraints_file})" 

870 ) 

871 pkg_id = BinaryPackageId(pkg_name, faux_version, arch) 

872 bin_data = BinaryPackage( 

873 faux_section, 

874 pkg_name, 

875 faux_version, 

876 arch, 

877 MultiArch.NO, 

878 ", ".join(deps), 

879 None, 

880 None, 

881 False, 

882 pkg_id, 

883 None, 

884 ) 

885 src_data.binaries.add(pkg_id) 

886 target_suite.binaries[arch][pkg_name] = bin_data 

887 pri_source_suite.binaries[arch][pkg_name] = bin_data 

888 self.all_binaries[pkg_id] = bin_data 

889 

890 return constraints 

891 

892 # Data reading/writing methods 

893 # ---------------------------- 

894 

895 def read_hints(self, hintsdir: str) -> None: 

896 """Read the hint commands from the specified directory 

897 

898 The hint commands are read from the files contained in the directory 

899 specified by the `hintsdir' parameter. 

900 The names of the files have to be the same as the authorized users 

901 for the hints. 

902 

903 The file contains rows with the format: 

904 

905 <command> <package-name>[/<version>] 

906 

907 The method returns a dictionary where the key is the command, and 

908 the value is the list of affected packages. 

909 """ 

910 

911 for who in self.HINTS.keys(): 

912 if who == "command-line": 

913 lines = self.options.hints and self.options.hints.split(";") or () 

914 filename = "<cmd-line>" 

915 self._hint_parser.parse_hints(who, self.HINTS[who], filename, lines) 

916 else: 

917 filename = os.path.join(hintsdir, who) 

918 if not os.path.isfile(filename): 918 ↛ 919line 918 didn't jump to line 919 because the condition on line 918 was never true

919 self.logger.error( 

920 "Cannot read hints list from %s, no such file!", filename 

921 ) 

922 continue 

923 self.logger.info("Loading hints list from %s", filename) 

924 with open(filename, encoding="utf-8") as f: 

925 self._hint_parser.parse_hints(who, self.HINTS[who], filename, f) 

926 

927 hints = self._hint_parser.hints 

928 

929 for x in ( 

930 "block", 

931 "block-all", 

932 "block-udeb", 

933 "unblock", 

934 "unblock-udeb", 

935 "force", 

936 "urgent", 

937 "remove", 

938 "age-days", 

939 ): 

940 z: dict[str | None, dict[str | None, tuple[Hint, str]]] = defaultdict(dict) 

941 for hint in hints[x]: 

942 package = hint.package 

943 architecture = hint.architecture 

944 key = (hint, hint.user) 

945 if ( 

946 package in z 

947 and architecture in z[package] 

948 and z[package][architecture] != key 

949 ): 

950 hint2 = z[package][architecture][0] 

951 if x in ("unblock", "unblock-udeb", "age-days"): 951 ↛ 983line 951 didn't jump to line 983 because the condition on line 951 was always true

952 assert hint.version is not None 

953 assert hint2.version is not None 

954 if apt_pkg.version_compare(hint2.version, hint.version) < 0: 

955 # This hint is for a newer version, so discard the old one 

956 self.logger.warning( 

957 "Overriding %s[%s] = ('%s', '%s', '%s') with ('%s', '%s', '%s')", 

958 x, 

959 package, 

960 hint2.version, 

961 hint2.architecture, 

962 hint2.user, 

963 hint.version, 

964 hint.architecture, 

965 hint.user, 

966 ) 

967 hint2.set_active(False) 

968 else: 

969 # This hint is for an older version, so ignore it in favour of the new one 

970 self.logger.warning( 

971 "Ignoring %s[%s] = ('%s', '%s', '%s'), ('%s', '%s', '%s') is higher or equal", 

972 x, 

973 package, 

974 hint.version, 

975 hint.architecture, 

976 hint.user, 

977 hint2.version, 

978 hint2.architecture, 

979 hint2.user, 

980 ) 

981 hint.set_active(False) 

982 else: 

983 self.logger.warning( 

984 "Overriding %s[%s] = ('%s', '%s') with ('%s', '%s')", 

985 x, 

986 package, 

987 hint2.user, 

988 hint2, 

989 hint.user, 

990 hint, 

991 ) 

992 hint2.set_active(False) 

993 

994 z[package][architecture] = key 

995 

996 for hint in hints["allow-uninst"]: 

997 if hint.architecture == "source": 

998 for arch in self.options.architectures: 

999 self.allow_uninst[arch].add(hint.package) 

1000 else: 

1001 assert hint.architecture is not None 

1002 self.allow_uninst[hint.architecture].add(hint.package) 

1003 

1004 # Sanity check the hints hash 

1005 if len(hints["block"]) == 0 and len(hints["block-udeb"]) == 0: 1005 ↛ 1006line 1005 didn't jump to line 1006 because the condition on line 1005 was never true

1006 self.logger.warning("WARNING: No block hints at all, not even udeb ones!") 

1007 

1008 # Remove all hints that were set inactive. 

1009 # We don't need to keep unused hints in memory. 

1010 hints.remove_inactive_hints() 

1011 

1012 def write_excuses(self) -> None: 

1013 """Produce and write the update excuses 

1014 

1015 This method handles the update excuses generation: the packages are 

1016 looked at to determine whether they are valid candidates. For the details 

1017 of this procedure, please refer to the module docstring. 

1018 """ 

1019 

1020 self.logger.info("Update Excuses generation started") 

1021 

1022 mi_factory = self._migration_item_factory 

1023 excusefinder = ExcuseFinder( 

1024 self.options, 

1025 self.suite_info, 

1026 self.all_binaries, 

1027 self.pkg_universe, 

1028 self._policy_engine, 

1029 mi_factory, 

1030 self.hints, 

1031 ) 

1032 

1033 excuses, upgrade_me = excusefinder.find_actionable_excuses() 

1034 self.excuses = excuses 

1035 

1036 # sort the list of candidates 

1037 self.upgrade_me = sorted(upgrade_me) 

1038 old_lib_removals = old_libraries( 

1039 mi_factory, self.suite_info, self.options.outofsync_arches 

1040 ) 

1041 self.upgrade_me.extend(old_lib_removals) 

1042 self.output_logger.info( 

1043 "List of old libraries added to upgrade_me (%d):", len(old_lib_removals) 

1044 ) 

1045 log_and_format_old_libraries(self.output_logger, old_lib_removals) 

1046 

1047 # write excuses to the output file 

1048 if not self.options.dry_run: 1048 ↛ 1065line 1048 didn't jump to line 1065 because the condition on line 1048 was always true

1049 self.logger.info("> Writing Excuses to %s", self.options.excuses_output) 

1050 write_excuses( 

1051 excuses, 

1052 self.options.excuses_output, 

1053 output_format=ExcusesOutputFormat.LEGACY_HTML, 

1054 ) 

1055 if hasattr(self.options, "excuses_yaml_output"): 1055 ↛ 1065line 1055 didn't jump to line 1065 because the condition on line 1055 was always true

1056 self.logger.info( 

1057 "> Writing YAML Excuses to %s", self.options.excuses_yaml_output 

1058 ) 

1059 write_excuses( 

1060 excuses, 

1061 self.options.excuses_yaml_output, 

1062 output_format=ExcusesOutputFormat.YAML, 

1063 ) 

1064 

1065 self.logger.info("Update Excuses generation completed") 

1066 

1067 # Upgrade run 

1068 # ----------- 

1069 

1070 def eval_nuninst( 

1071 self, 

1072 nuninst: dict[str, set[str]], 

1073 original: dict[str, set[str]] | None = None, 

1074 ) -> str: 

1075 """Return a string which represents the uninstallability counters 

1076 

1077 This method returns a string which represents the uninstallability 

1078 counters reading the uninstallability statistics `nuninst` and, if 

1079 present, merging the results with the `original` one. 

1080 

1081 An example of the output string is: 

1082 1+2: i-0:a-0:a-0:h-0:i-1:m-0:m-0:p-0:a-0:m-0:s-2:s-0 

1083 

1084 where the first part is the number of broken packages in non-break 

1085 architectures + the total number of broken packages for all the 

1086 architectures. 

1087 """ 

1088 res = [] 

1089 total = 0 

1090 totalbreak = 0 

1091 for arch in self.options.architectures: 

1092 if arch in nuninst: 1092 ↛ 1094line 1092 didn't jump to line 1094 because the condition on line 1092 was always true

1093 n = len(nuninst[arch]) 

1094 elif original and arch in original: 

1095 n = len(original[arch]) 

1096 else: 

1097 continue 

1098 if arch in self.options.break_arches: 

1099 totalbreak = totalbreak + n 

1100 else: 

1101 total = total + n 

1102 res.append(f"{arch[0]}-{n}") 

1103 return "%d+%d: %s" % (total, totalbreak, ":".join(res)) 

1104 

1105 def iter_packages( 

1106 self, 

1107 packages: list[MigrationItem], 

1108 selected: list[MigrationItem], 

1109 nuninst: dict[str, set[str]] | None = None, 

1110 ) -> tuple[dict[str, set[str]] | None, list[MigrationItem]]: 

1111 """Iter on the list of actions and apply them one-by-one 

1112 

1113 This method applies the changes from `packages` to testing, checking the uninstallability 

1114 counters for every action performed. If the action does not improve them, it is reverted. 

1115 The method returns the new uninstallability counters and the remaining actions if the 

1116 final result is successful, otherwise (None, []). 

1117 

1118 :param selected: list of MigrationItem? 

1119 :param nuninst: dict with sets ? of ? per architecture 

1120 """ 

1121 assert self.suite_info is not None # for type checking 

1122 group_info = {} 

1123 rescheduled_packages = packages 

1124 maybe_rescheduled_packages: list[MigrationItem] = [] 

1125 output_logger = self.output_logger 

1126 solver = InstallabilitySolver(self.pkg_universe, self._inst_tester) 

1127 mm = self._migration_manager 

1128 target_suite = self.suite_info.target_suite 

1129 

1130 for y in sorted((y for y in packages), key=attrgetter("uvname")): 

1131 try: 

1132 _, updates, rms, _ = mm.compute_groups(y) 

1133 result = (y, sorted(updates), sorted(rms)) 

1134 group_info[y] = result 

1135 except MigrationConstraintException as e: 

1136 rescheduled_packages.remove(y) 

1137 output_logger.info("not adding package to list: %s", y.package) 

1138 output_logger.info(" got exception: %r", e) 

1139 

1140 if nuninst: 

1141 nuninst_orig = nuninst 

1142 else: 

1143 nuninst_orig = self.nuninst_orig 

1144 

1145 nuninst_last_accepted = nuninst_orig 

1146 

1147 output_logger.info( 

1148 "recur: [] %s %d/0", ",".join(x.uvname for x in selected), len(packages) 

1149 ) 

1150 while rescheduled_packages: 

1151 worklist = solver.solve_groups(group_info[x] for x in rescheduled_packages) 

1152 rescheduled_packages = [] 

1153 

1154 worklist.reverse() 

1155 

1156 while worklist: 

1157 comp = worklist.pop() 

1158 comp_name = " ".join(item.uvname for item in comp) 

1159 output_logger.info("trying: %s", comp_name) 

1160 with mm.start_transaction() as transaction: 

1161 accepted = False 

1162 try: 

1163 ( 

1164 accepted, 

1165 nuninst_after, 

1166 failed_arch, 

1167 new_cruft, 

1168 ) = mm.migrate_items_to_target_suite( 

1169 comp, nuninst_last_accepted 

1170 ) 

1171 if accepted: 

1172 selected.extend(comp) 

1173 transaction.commit() 

1174 output_logger.info("accepted: %s", comp_name) 

1175 output_logger.info( 

1176 " ori: %s", self.eval_nuninst(nuninst_orig) 

1177 ) 

1178 output_logger.info( 

1179 " pre: %s", self.eval_nuninst(nuninst_last_accepted) 

1180 ) 

1181 output_logger.info( 

1182 " now: %s", self.eval_nuninst(nuninst_after) 

1183 ) 

1184 if new_cruft: 

1185 output_logger.info( 

1186 " added new cruft items to list: %s", 

1187 " ".join(x.uvname for x in sorted(new_cruft)), 

1188 ) 

1189 

1190 if len(selected) <= 20: 

1191 output_logger.info( 

1192 " all: %s", " ".join(x.uvname for x in selected) 

1193 ) 

1194 else: 

1195 output_logger.info( 

1196 " most: (%d) .. %s", 

1197 len(selected), 

1198 " ".join(x.uvname for x in selected[-20:]), 

1199 ) 

1200 if self.options.check_consistency_level >= 3: 1200 ↛ 1201line 1200 didn't jump to line 1201 because the condition on line 1200 was never true

1201 target_suite.check_suite_source_pkg_consistency( 

1202 "iter_packages after commit" 

1203 ) 

1204 nuninst_last_accepted = nuninst_after 

1205 for cruft_item in new_cruft: 

1206 try: 

1207 _, updates, rms, _ = mm.compute_groups(cruft_item) 

1208 result = (cruft_item, sorted(updates), sorted(rms)) 

1209 group_info[cruft_item] = result 

1210 worklist.append([cruft_item]) 

1211 except MigrationConstraintException as e: 

1212 output_logger.info( 

1213 " got exception adding cruft item %s to list: %r", 

1214 cruft_item.uvname, 

1215 e, 

1216 ) 

1217 rescheduled_packages.extend(maybe_rescheduled_packages) 

1218 maybe_rescheduled_packages.clear() 

1219 else: 

1220 transaction.rollback() 

1221 assert failed_arch # type checking 

1222 broken = sorted( 

1223 b 

1224 for b in nuninst_after[failed_arch] 

1225 if b not in nuninst_last_accepted[failed_arch] 

1226 ) 

1227 compare_nuninst = None 

1228 if any( 

1229 item for item in comp if item.architecture != "source" 

1230 ): 

1231 compare_nuninst = nuninst_last_accepted 

1232 # NB: try_migration already reverted this for us, so just print the results and move on 

1233 output_logger.info( 

1234 "skipped: %s (%d, %d, %d)", 

1235 comp_name, 

1236 len(rescheduled_packages), 

1237 len(maybe_rescheduled_packages), 

1238 len(worklist), 

1239 ) 

1240 output_logger.info( 

1241 " got: %s", 

1242 self.eval_nuninst(nuninst_after, compare_nuninst), 

1243 ) 

1244 output_logger.info( 

1245 " * %s: %s", failed_arch, ", ".join(broken) 

1246 ) 

1247 if self.options.check_consistency_level >= 3: 1247 ↛ 1248line 1247 didn't jump to line 1248 because the condition on line 1247 was never true

1248 target_suite.check_suite_source_pkg_consistency( 

1249 "iter_package after rollback (not accepted)" 

1250 ) 

1251 

1252 except MigrationConstraintException as e: 

1253 transaction.rollback() 

1254 output_logger.info( 

1255 "skipped: %s (%d, %d, %d)", 

1256 comp_name, 

1257 len(rescheduled_packages), 

1258 len(maybe_rescheduled_packages), 

1259 len(worklist), 

1260 ) 

1261 output_logger.info(" got exception: %r", e) 

1262 if self.options.check_consistency_level >= 3: 1262 ↛ 1263line 1262 didn't jump to line 1263 because the condition on line 1262 was never true

1263 target_suite.check_suite_source_pkg_consistency( 

1264 "iter_package after rollback (MigrationConstraintException)" 

1265 ) 

1266 

1267 if not accepted: 

1268 if len(comp) > 1: 

1269 output_logger.info( 

1270 " - splitting the component into single items and retrying them" 

1271 ) 

1272 worklist.extend([item] for item in comp) 

1273 else: 

1274 maybe_rescheduled_packages.append(comp[0]) 

1275 

1276 output_logger.info(" finish: [%s]", ",".join(x.uvname for x in selected)) 

1277 output_logger.info("endloop: %s", self.eval_nuninst(self.nuninst_orig)) 

1278 output_logger.info(" now: %s", self.eval_nuninst(nuninst_last_accepted)) 

1279 format_and_log_uninst( 

1280 output_logger, 

1281 self.options.architectures, 

1282 newly_uninst(self.nuninst_orig, nuninst_last_accepted), 

1283 ) 

1284 output_logger.info("") 

1285 

1286 return (nuninst_last_accepted, maybe_rescheduled_packages) 

1287 

1288 def do_all( 

1289 self, 

1290 hinttype: str | None = None, 

1291 init: list[MigrationItem] | None = None, 

1292 actions: list[MigrationItem] | None = None, 

1293 ) -> None: 

1294 """Testing update runner 

1295 

1296 This method tries to update testing checking the uninstallability 

1297 counters before and after the actions to decide if the update was 

1298 successful or not. 

1299 """ 

1300 selected = [] 

1301 if actions: 

1302 upgrade_me = actions[:] 

1303 else: 

1304 upgrade_me = self.upgrade_me[:] 

1305 nuninst_start = self.nuninst_orig 

1306 output_logger = self.output_logger 

1307 target_suite = self.suite_info.target_suite 

1308 

1309 # these are special parameters for hints processing 

1310 force = False 

1311 recurse = True 

1312 nuninst_end = None 

1313 extra: list[MigrationItem] = [] 

1314 mm = self._migration_manager 

1315 

1316 if hinttype == "easy" or hinttype == "force-hint": 

1317 force = hinttype == "force-hint" 

1318 recurse = False 

1319 

1320 # if we have a list of initial packages, check them 

1321 if init: 

1322 for x in init: 

1323 if x not in upgrade_me: 

1324 output_logger.warning( 

1325 "failed: %s is not a valid candidate (or it already migrated)", 

1326 x.uvname, 

1327 ) 

1328 return None 

1329 selected.append(x) 

1330 upgrade_me.remove(x) 

1331 

1332 output_logger.info("start: %s", self.eval_nuninst(nuninst_start)) 

1333 output_logger.info("orig: %s", self.eval_nuninst(nuninst_start)) 

1334 

1335 if not (init and not force): 

1336 # No "outer" transaction needed as we will never need to rollback 

1337 # (e.g. "force-hint" or a regular "main run"). Emulate the start_transaction 

1338 # call from the MigrationManager, so the rest of the code follows the 

1339 # same flow regardless of whether we need the transaction or not. 

1340 

1341 @contextlib.contextmanager 

1342 def _start_transaction() -> ( 

1343 Generator[Optional["MigrationTransactionState"]] 

1344 ): 

1345 yield None 

1346 

1347 else: 

1348 # We will need to be able to roll back (e.g. easy or a "hint"-hint) 

1349 _start_transaction = mm.start_transaction 

1350 

1351 with _start_transaction() as transaction: 

1352 if init: 

1353 # init => a hint (e.g. "easy") - so do the hint run 

1354 (_, nuninst_end, _, new_cruft) = mm.migrate_items_to_target_suite( 

1355 selected, self.nuninst_orig, stop_on_first_regression=False 

1356 ) 

1357 

1358 if recurse: 

1359 # Ensure upgrade_me and selected do not overlap, if we 

1360 # follow-up with a recurse ("hint"-hint). 

1361 selected_set = set(selected) 

1362 upgrade_me = [x for x in upgrade_me if x not in selected_set] 

1363 else: 

1364 # On non-recursive hints check for cruft and purge it proactively in case it "fixes" the hint. 

1365 cruft = [x for x in upgrade_me if x.is_cruft_removal] 

1366 if new_cruft: 

1367 output_logger.info( 

1368 "Change added new cruft items to list: %s", 

1369 " ".join(x.uvname for x in sorted(new_cruft)), 

1370 ) 

1371 cruft.extend(new_cruft) 

1372 if cruft: 

1373 output_logger.info("Checking if changes enables cruft removal") 

1374 (nuninst_end, remaining_cruft) = self.iter_packages( 

1375 cruft, selected, nuninst=nuninst_end 

1376 ) 

1377 output_logger.info( 

1378 "Removed %d of %d cruft item(s) after the changes", 

1379 len(cruft) - len(remaining_cruft), 

1380 len(cruft), 

1381 ) 

1382 new_cruft.difference_update(remaining_cruft) 

1383 

1384 # Add new cruft items regardless of whether we recurse. A future run might clean 

1385 # them for us. 

1386 upgrade_me.extend(new_cruft) 

1387 

1388 if recurse: 

1389 # Either the main run or the recursive run of a "hint"-hint. 

1390 (nuninst_end, extra) = self.iter_packages( 

1391 upgrade_me, selected, nuninst=nuninst_end 

1392 ) 

1393 

1394 assert nuninst_end is not None 

1395 nuninst_end_str = self.eval_nuninst(nuninst_end) 

1396 

1397 if not recurse: 

1398 # easy or force-hint 

1399 output_logger.info("easy: %s", nuninst_end_str) 

1400 

1401 if not force: 

1402 format_and_log_uninst( 

1403 self.output_logger, 

1404 self.options.architectures, 

1405 newly_uninst(nuninst_start, nuninst_end), 

1406 ) 

1407 

1408 if force: 

1409 # Force implies "unconditionally better" 

1410 better = True 

1411 else: 

1412 break_arches: set[str] = set(self.options.break_arches) 

1413 if all(x.architecture in break_arches for x in selected): 

1414 # If we only migrated items from break-arches, then we 

1415 # do not allow any regressions on these architectures. 

1416 # This usually only happens with hints 

1417 break_arches = set() 

1418 better = is_nuninst_asgood_generous( 

1419 self.constraints, 

1420 self.allow_uninst, 

1421 self.options.architectures, 

1422 self.nuninst_orig, 

1423 nuninst_end, 

1424 break_arches, 

1425 ) 

1426 

1427 if better: 

1428 # Result accepted either by force or by being better than the original result. 

1429 output_logger.info( 

1430 "final: %s", ",".join(sorted(x.uvname for x in selected)) 

1431 ) 

1432 output_logger.info("start: %s", self.eval_nuninst(nuninst_start)) 

1433 output_logger.info(" orig: %s", self.eval_nuninst(self.nuninst_orig)) 

1434 output_logger.info(" end: %s", nuninst_end_str) 

1435 if force: 

1436 broken = newly_uninst(nuninst_start, nuninst_end) 

1437 if broken: 

1438 output_logger.warning("force breaks:") 

1439 format_and_log_uninst( 

1440 self.output_logger, 

1441 self.options.architectures, 

1442 broken, 

1443 loglevel=logging.WARNING, 

1444 ) 

1445 else: 

1446 output_logger.info("force did not break any packages") 

1447 output_logger.info( 

1448 "SUCCESS (%d/%d)", len(actions or self.upgrade_me), len(extra) 

1449 ) 

1450 self.nuninst_orig = nuninst_end 

1451 self.all_selected += selected 

1452 if transaction: 

1453 transaction.commit() 

1454 if self.options.check_consistency_level >= 2: 1454 ↛ 1458line 1454 didn't jump to line 1458 because the condition on line 1454 was always true

1455 target_suite.check_suite_source_pkg_consistency( 

1456 "do_all after commit" 

1457 ) 

1458 if not actions: 

1459 if recurse: 

1460 self.upgrade_me = extra 

1461 else: 

1462 selected_set = set(selected) 

1463 self.upgrade_me = [ 

1464 x for x in self.upgrade_me if x not in selected_set 

1465 ] 

1466 else: 

1467 output_logger.info("FAILED\n") 

1468 if not transaction: 1468 ↛ 1472line 1468 didn't jump to line 1472 because the condition on line 1468 was never true

1469 # if we 'FAILED', but we cannot rollback, we will probably 

1470 # leave a broken state behind 

1471 # this should not happen 

1472 raise AssertionError("do_all FAILED but no transaction to rollback") 

1473 transaction.rollback() 

1474 if self.options.check_consistency_level >= 2: 1474 ↛ 1351line 1474 didn't jump to line 1351

1475 target_suite.check_suite_source_pkg_consistency( 

1476 "do_all after rollback" 

1477 ) 

1478 

1479 output_logger.info("") 

1480 

1481 def assert_nuninst_is_correct(self) -> None: 

1482 self.logger.info("> Update complete - Verifying non-installability counters") 

1483 

1484 cached_nuninst = self.nuninst_orig 

1485 self._inst_tester.compute_installability() 

1486 computed_nuninst = compile_nuninst( 

1487 self.suite_info.target_suite, 

1488 self.options.architectures, 

1489 self.options.nobreakall_arches, 

1490 ) 

1491 if cached_nuninst != computed_nuninst: # pragma: no cover 

1492 only_on_break_archs = True 

1493 msg_l = [ 

1494 "==================== NUNINST OUT OF SYNC =========================" 

1495 ] 

1496 for arch in self.options.architectures: 

1497 expected_nuninst = set(cached_nuninst[arch]) 

1498 actual_nuninst = set(computed_nuninst[arch]) 

1499 false_negatives = actual_nuninst - expected_nuninst 

1500 false_positives = expected_nuninst - actual_nuninst 

1501 # Britney does not quite work correctly with 

1502 # break/fucked arches, so ignore issues there for now. 

1503 if ( 

1504 false_negatives or false_positives 

1505 ) and arch not in self.options.break_arches: 

1506 only_on_break_archs = False 

1507 if false_negatives: 

1508 msg_l.append(f" {arch} - unnoticed nuninst: {str(false_negatives)}") 

1509 if false_positives: 

1510 msg_l.append(f" {arch} - invalid nuninst: {str(false_positives)}") 

1511 if false_negatives or false_positives: 

1512 msg_l.append( 

1513 f" {arch} - actual nuninst: {str(sorted(actual_nuninst))}" 

1514 ) 

1515 msg_l.append(msg_l[0]) 

1516 for msg in msg_l: 

1517 if only_on_break_archs: 

1518 self.logger.warning(msg) 

1519 else: 

1520 self.logger.error(msg) 

1521 if not only_on_break_archs: 

1522 raise AssertionError("NUNINST OUT OF SYNC") 

1523 else: 

1524 self.logger.warning("Nuninst is out of sync on some break arches") 

1525 

1526 self.logger.info("> All non-installability counters are ok") 

1527 

1528 def upgrade_testing(self) -> None: 

1529 """Upgrade testing using the packages from the source suites 

1530 

1531 This method tries to upgrade testing using the packages from the 

1532 source suites. 

1533 Before running the do_all method, it tries the easy and force-hint 

1534 commands. 

1535 """ 

1536 

1537 output_logger = self.output_logger 

1538 self.logger.info("Starting the upgrade test") 

1539 output_logger.info( 

1540 "Generated on: %s", 

1541 time.strftime("%Y.%m.%d %H:%M:%S %z", time.gmtime(time.time())), 

1542 ) 

1543 output_logger.info("Arch order is: %s", ", ".join(self.options.architectures)) 

1544 

1545 if not self.options.actions: 1545 ↛ 1556line 1545 didn't jump to line 1556 because the condition on line 1545 was always true

1546 # process `easy' hints 

1547 for x in self.hints["easy"]: 

1548 self.do_hint("easy", x.user, x.packages) 

1549 

1550 # process `force-hint' hints 

1551 for x in self.hints["force-hint"]: 

1552 self.do_hint("force-hint", x.user, x.packages) 

1553 

1554 # run the first round of the upgrade 

1555 # - do separate runs for break arches 

1556 allpackages = [] 

1557 normpackages = self.upgrade_me[:] 

1558 archpackages = {} 

1559 for a in self.options.break_arches: 

1560 archpackages[a] = [p for p in normpackages if p.architecture == a] 

1561 normpackages = [p for p in normpackages if p.architecture != a] 

1562 self.upgrade_me = normpackages 

1563 output_logger.info("info: main run") 

1564 self.do_all() 

1565 allpackages += self.upgrade_me 

1566 for a in self.options.break_arches: 

1567 backup = self.options.break_arches 

1568 self.options.break_arches = " ".join( 

1569 x for x in self.options.break_arches if x != a 

1570 ) 

1571 self.upgrade_me = archpackages[a] 

1572 output_logger.info("info: broken arch run for %s", a) 

1573 self.do_all() 

1574 allpackages += self.upgrade_me 

1575 self.options.break_arches = backup 

1576 self.upgrade_me = allpackages 

1577 

1578 if self.options.actions: 1578 ↛ 1579line 1578 didn't jump to line 1579 because the condition on line 1578 was never true

1579 self.printuninstchange() 

1580 return 

1581 

1582 # process `hint' hints 

1583 hintcnt = 0 

1584 for x in self.hints["hint"][:50]: 

1585 if hintcnt > 50: 1585 ↛ 1586line 1585 didn't jump to line 1586 because the condition on line 1585 was never true

1586 output_logger.info("Skipping remaining hints...") 

1587 break 

1588 if self.do_hint("hint", x.user, x.packages): 1588 ↛ 1584line 1588 didn't jump to line 1584 because the condition on line 1588 was always true

1589 hintcnt += 1 

1590 

1591 # run the auto hinter 

1592 self.run_auto_hinter() 

1593 

1594 if getattr(self.options, "remove_obsolete", "yes") == "yes": 

1595 # obsolete source packages 

1596 # a package is obsolete if none of the binary packages in testing 

1597 # are built by it 

1598 self.logger.info( 

1599 "> Removing obsolete source packages from the target suite" 

1600 ) 

1601 # local copies for performance 

1602 target_suite = self.suite_info.target_suite 

1603 sources_t = target_suite.sources 

1604 binaries_t = target_suite.binaries 

1605 mi_factory = self._migration_item_factory 

1606 used = { 

1607 binaries_t[arch][binary].source 

1608 for arch in binaries_t 

1609 for binary in binaries_t[arch] 

1610 if not binary.endswith("-faux-build-depends") 

1611 } 

1612 removals = [ 

1613 mi_factory.parse_item( 

1614 f"-{source}/{sources_t[source].version}", auto_correct=False 

1615 ) 

1616 for source in sources_t 

1617 if source not in used 

1618 ] 

1619 if removals: 

1620 output_logger.info( 

1621 "Removing obsolete source packages from the target suite (%d):", 

1622 len(removals), 

1623 ) 

1624 self.do_all(actions=removals) 

1625 

1626 # smooth updates 

1627 removals = old_libraries( 

1628 self._migration_item_factory, self.suite_info, self.options.outofsync_arches 

1629 ) 

1630 if removals: 

1631 output_logger.info( 

1632 "Removing packages left in the target suite (e.g. smooth updates or cruft)" 

1633 ) 

1634 log_and_format_old_libraries(self.output_logger, removals) 

1635 self.do_all(actions=removals) 

1636 removals = old_libraries( 

1637 self._migration_item_factory, 

1638 self.suite_info, 

1639 self.options.outofsync_arches, 

1640 ) 

1641 

1642 output_logger.info( 

1643 "List of old libraries in the target suite (%d):", len(removals) 

1644 ) 

1645 log_and_format_old_libraries(self.output_logger, removals) 

1646 

1647 self.printuninstchange() 

1648 if self.options.check_consistency_level >= 1: 1648 ↛ 1654line 1648 didn't jump to line 1654 because the condition on line 1648 was always true

1649 target_suite = self.suite_info.target_suite 

1650 self.assert_nuninst_is_correct() 

1651 target_suite.check_suite_source_pkg_consistency("end") 

1652 

1653 # output files 

1654 if self.options.heidi_output and not self.options.dry_run: 1654 ↛ 1668line 1654 didn't jump to line 1668 because the condition on line 1654 was always true

1655 target_suite = self.suite_info.target_suite 

1656 

1657 # write HeidiResult 

1658 self.logger.info("Writing Heidi results to %s", self.options.heidi_output) 

1659 write_heidi( 

1660 self.options.heidi_output, 

1661 target_suite, 

1662 outofsync_arches=self.options.outofsync_arches, 

1663 ) 

1664 

1665 self.logger.info("Writing delta to %s", self.options.heidi_delta_output) 

1666 write_heidi_delta(self.options.heidi_delta_output, self.all_selected) 

1667 

1668 self.logger.info("Test completed!") 

1669 

1670 def printuninstchange(self) -> None: 

1671 self.logger.info("Checking for newly uninstallable packages") 

1672 uninst = newly_uninst(self.nuninst_orig_save, self.nuninst_orig) 

1673 

1674 if uninst: 

1675 self.output_logger.info("") 

1676 self.output_logger.info( 

1677 "Newly uninstallable packages in the target suite (arch:all on BREAKALL_ARCHES not shown)" 

1678 ) 

1679 format_and_log_uninst( 

1680 self.output_logger, 

1681 self.options.architectures, 

1682 uninst, 

1683 loglevel=logging.WARNING, 

1684 ) 

1685 

1686 def hint_tester(self) -> None: 

1687 """Run a command line interface to test hints 

1688 

1689 This method provides a command line interface for the release team to 

1690 try hints and evaluate the results. 

1691 """ 

1692 import readline 

1693 

1694 from britney2.completer import Completer 

1695 

1696 histfile = os.path.expanduser("~/.britney2_history") 

1697 if os.path.exists(histfile): 

1698 readline.read_history_file(histfile) 

1699 

1700 readline.parse_and_bind("tab: complete") 

1701 readline.set_completer(Completer(self).completer) 

1702 # Package names can contain "-" and we use "/" in our presentation of them as well, 

1703 # so ensure readline does not split on these characters. 

1704 readline.set_completer_delims( 

1705 readline.get_completer_delims().replace("-", "").replace("/", "") 

1706 ) 

1707 

1708 known_hints = self._hint_parser.registered_hint_names 

1709 

1710 print("Britney hint tester") 

1711 print() 

1712 print( 

1713 "Besides inputting known britney hints, the follow commands are also available" 

1714 ) 

1715 print(" * quit/exit - terminates the shell") 

1716 print( 

1717 " * python-console - jump into an interactive python shell (with the current loaded dataset)" 

1718 ) 

1719 print() 

1720 

1721 while True: 

1722 # read the command from the command line 

1723 try: 

1724 user_input = input("britney> ").split() 

1725 except EOFError: 

1726 print("") 

1727 break 

1728 except KeyboardInterrupt: 

1729 print("") 

1730 continue 

1731 match user_input: 

1732 case ("quit" | "exit", *_): 

1733 # quit the hint tester 

1734 break 

1735 case ("python-console", *_): 

1736 try: 

1737 import britney2.console 

1738 except ImportError as e: 

1739 print(f"Failed to import britney.console module: {e!r}") 

1740 continue 

1741 britney2.console.run_python_console(self) 

1742 print("Returning to the britney hint-tester console") 

1743 # run a hint 

1744 case ("easy" | "hint" | "force-hint" as choice, *items): 

1745 mi_factory = self._migration_item_factory 

1746 try: 

1747 self.do_hint( 

1748 choice, 

1749 "hint-tester", 

1750 list(mi_factory.parse_items(items)), 

1751 ) 

1752 self.printuninstchange() 

1753 except KeyboardInterrupt: 

1754 continue 

1755 case (str() as hint, *_) if hint in known_hints: 

1756 self._hint_parser.parse_hints( 

1757 "hint-tester", self.HINTS_ALL, "<stdin>", [" ".join(user_input)] 

1758 ) 

1759 self.write_excuses() 

1760 

1761 try: 

1762 readline.write_history_file(histfile) 

1763 except OSError as e: 

1764 self.logger.warning("Could not write %s: %s", histfile, e) 

1765 

1766 def do_hint(self, hinttype: str, who: str, pkgvers: list[MigrationItem]) -> bool: 

1767 """Process hints 

1768 

1769 This method process `easy`, `hint` and `force-hint` hints. If the 

1770 requested version is not in the relevant source suite, then the hint 

1771 is skipped. 

1772 """ 

1773 

1774 output_logger = self.output_logger 

1775 

1776 self.logger.info("> Processing '%s' hint from %s", hinttype, who) 

1777 output_logger.info( 

1778 "Trying %s from %s: %s", 

1779 hinttype, 

1780 who, 

1781 " ".join(f"{x.uvname}/{x.version}" for x in pkgvers), 

1782 ) 

1783 

1784 issues = [] 

1785 # loop on the requested packages and versions 

1786 for pkg in pkgvers: 

1787 # skip removal requests 

1788 if pkg.is_removal: 

1789 continue 

1790 

1791 suite = pkg.suite 

1792 

1793 assert pkg.version is not None 

1794 source = suite.sources.get(pkg.package) 

1795 if source is None: 1795 ↛ 1796line 1795 didn't jump to line 1796 because the condition on line 1795 was never true

1796 issues.append(f"Source {pkg.package} has no version in {suite.name}") 

1797 elif apt_pkg.version_compare(source.version, pkg.version) != 0: 1797 ↛ 1798line 1797 didn't jump to line 1798 because the condition on line 1797 was never true

1798 issues.append( 

1799 f"Version mismatch, {pkg.package} {pkg.version} != {source.version}" 

1800 ) 

1801 if issues: 1801 ↛ 1802line 1801 didn't jump to line 1802 because the condition on line 1801 was never true

1802 output_logger.warning("%s: Not using hint", ", ".join(issues)) 

1803 return False 

1804 

1805 self.do_all(hinttype, pkgvers) 

1806 return True 

1807 

1808 def get_auto_hinter_hints( 

1809 self, upgrade_me: list[MigrationItem] 

1810 ) -> tuple[list[frozenset[MigrationItem]], list[frozenset[MigrationItem]]]: 

1811 """Auto-generate "easy" hints. 

1812 

1813 This method attempts to generate "easy" hints for sets of packages which 

1814 must migrate together. Beginning with a package which does not depend on 

1815 any other package (in terms of excuses), a list of dependencies and 

1816 reverse dependencies is recursively created. 

1817 

1818 Once all such lists have been generated, any which are subsets of other 

1819 lists are ignored in favour of the larger lists. The remaining lists are 

1820 then attempted in turn as "easy" hints. 

1821 

1822 We also try to auto hint circular dependencies analyzing the update 

1823 excuses relationships. If they build a circular dependency, which we already 

1824 know as not-working with the standard do_all algorithm, try to `easy` them. 

1825 """ 

1826 self.logger.info("> Processing hints from the auto hinter") 

1827 

1828 sources_t = self.suite_info.target_suite.sources 

1829 excuses = self.excuses 

1830 

1831 def excuse_still_valid(excuse: "Excuse") -> bool: 

1832 source = excuse.source 

1833 assert isinstance(excuse.item, MigrationItem) 

1834 arch = excuse.item.architecture 

1835 # TODO for binNMUs, this check is always ok, even if the item 

1836 # migrated already 

1837 valid = ( 

1838 arch != "source" 

1839 or source not in sources_t 

1840 or sources_t[source].version != excuse.ver[1] 

1841 ) 

1842 # TODO migrated items should be removed from upgrade_me, so this 

1843 # should not happen 

1844 if not valid: 1844 ↛ 1845line 1844 didn't jump to line 1845 because the condition on line 1844 was never true

1845 raise AssertionError(f"excuse no longer valid {excuse.item}") 

1846 return valid 

1847 

1848 # consider only excuses which are valid candidates and still relevant. 

1849 valid_excuses = frozenset( 

1850 e.name 

1851 for e in excuses.values() 

1852 if e.item in upgrade_me and excuse_still_valid(e) 

1853 ) 

1854 excuses_deps = { 

1855 name: valid_excuses.intersection(excuse.get_deps()) 

1856 for name, excuse in excuses.items() 

1857 if name in valid_excuses 

1858 } 

1859 excuses_rdeps = defaultdict(set) 

1860 for name, deps in excuses_deps.items(): 

1861 for dep in deps: 

1862 excuses_rdeps[dep].add(name) 

1863 

1864 # loop on them 

1865 candidates = [] 

1866 mincands = [] 

1867 seen_hints = set() 

1868 for e in valid_excuses: 

1869 excuse = excuses[e] 

1870 if not excuse.get_deps(): 

1871 assert isinstance(excuse.item, MigrationItem) 

1872 items = [excuse.item] 

1873 orig_size = 1 

1874 looped = False 

1875 seen_items = set() 

1876 seen_items.update(items) 

1877 

1878 for item in items: 

1879 assert isinstance(item, MigrationItem) 

1880 # excuses which depend on "item" or are depended on by it 

1881 new_items = { 

1882 excuses[x].item 

1883 for x in chain( 

1884 excuses_deps[item.name], excuses_rdeps[item.name] 

1885 ) 

1886 } 

1887 new_items -= seen_items 

1888 items.extend(new_items) 

1889 seen_items.update(new_items) 

1890 

1891 if not looped and len(items) > 1: 

1892 orig_size = len(items) 

1893 h = frozenset(seen_items) 

1894 if h not in seen_hints: 1894 ↛ 1897line 1894 didn't jump to line 1897 because the condition on line 1894 was always true

1895 mincands.append(h) 

1896 seen_hints.add(h) 

1897 looped = True 

1898 if len(items) != orig_size: 1898 ↛ 1899line 1898 didn't jump to line 1899 because the condition on line 1898 was never true

1899 h = frozenset(seen_items) 

1900 if h != mincands[-1] and h not in seen_hints: 

1901 candidates.append(h) 

1902 seen_hints.add(h) 

1903 return (candidates, mincands) 

1904 

1905 def run_auto_hinter(self) -> None: 

1906 for lst in self.get_auto_hinter_hints(self.upgrade_me): 

1907 for hint in lst: 

1908 self.do_hint("easy", "autohinter", sorted(hint)) 

1909 

1910 def nuninst_arch_report(self, nuninst: dict[str, set[str]], arch: str) -> None: 

1911 """Print a report of uninstallable packages for one architecture.""" 

1912 all = defaultdict(set) 

1913 binaries_t = self.suite_info.target_suite.binaries 

1914 for p in nuninst[arch]: 

1915 pkg = binaries_t[arch][p] 

1916 all[(pkg.source, pkg.source_version)].add(p) 

1917 

1918 print(f"* {arch}") 

1919 

1920 for (src, ver), pkgs in sorted(all.items()): 

1921 print(" {} ({}): {}".format(src, ver, " ".join(sorted(pkgs)))) 

1922 

1923 print() 

1924 

1925 def _remove_archall_faux_packages(self) -> None: 

1926 """Remove faux packages added for the excuses phase 

1927 

1928 To prevent binary packages from going missing while they are listed by 

1929 their source package we add bin:faux packages during reading in the 

1930 Sources. They are used during the excuses phase to prevent packages 

1931 from becoming candidates. However, they interfere in complex ways 

1932 during the installability phase, so instead of having all code during 

1933 migration be aware of this excuses phase implementation detail, let's 

1934 remove them again. 

1935 

1936 """ 

1937 if not self.options.archall_inconsistency_allowed: 

1938 all_binaries = self.all_binaries 

1939 faux_a = {x for x in all_binaries.keys() if x.architecture == "faux"} 

1940 for pkg_a in faux_a: 

1941 del all_binaries[pkg_a] 

1942 

1943 for suite in self.suite_info._suites.values(): 

1944 for arch in suite.binaries.keys(): 

1945 binaries = suite.binaries[arch] 

1946 faux_b = { 

1947 x for x in binaries if binaries[x].pkg_id.architecture == "faux" 

1948 } 

1949 for pkg_b in faux_b: 

1950 del binaries[pkg_b] 

1951 sources = suite.sources 

1952 for src in sources.keys(): 

1953 faux_s = { 

1954 x for x in sources[src].binaries if x.architecture == "faux" 

1955 } 

1956 sources[src].binaries -= faux_s 

1957 

1958 def main(self) -> None: 

1959 """Main method 

1960 

1961 This is the entry point for the class: it includes the list of calls 

1962 for the member methods which will produce the output files. 

1963 """ 

1964 # if running in --print-uninst mode, quit 

1965 if self.options.print_uninst: 1965 ↛ 1966line 1965 didn't jump to line 1966 because the condition on line 1965 was never true

1966 return 

1967 # if no actions are provided, build the excuses and sort them 

1968 elif not self.options.actions: 1968 ↛ 1972line 1968 didn't jump to line 1972 because the condition on line 1968 was always true

1969 self.write_excuses() 

1970 # otherwise, use the actions provided by the command line 

1971 else: 

1972 self.upgrade_me = self.options.actions.split() 

1973 

1974 self._remove_archall_faux_packages() 

1975 

1976 if self.options.compute_migrations or self.options.hint_tester: 

1977 if self.options.dry_run: 1977 ↛ 1978line 1977 didn't jump to line 1978 because the condition on line 1977 was never true

1978 self.logger.info( 

1979 "Upgrade output not (also) written to a separate file" 

1980 " as this is a dry-run." 

1981 ) 

1982 elif hasattr(self.options, "upgrade_output"): 1982 ↛ 1992line 1982 didn't jump to line 1992 because the condition on line 1982 was always true

1983 upgrade_output = getattr(self.options, "upgrade_output") 

1984 file_handler = logging.FileHandler( 

1985 upgrade_output, mode="w", encoding="utf-8" 

1986 ) 

1987 output_formatter = logging.Formatter("%(message)s") 

1988 file_handler.setFormatter(output_formatter) 

1989 self.output_logger.addHandler(file_handler) 

1990 self.logger.info("Logging upgrade output to %s", upgrade_output) 

1991 else: 

1992 self.logger.info( 

1993 "Upgrade output not (also) written to a separate file" 

1994 " as the UPGRADE_OUTPUT configuration is not provided." 

1995 ) 

1996 

1997 # run the hint tester 

1998 if self.options.hint_tester: 1998 ↛ 1999line 1998 didn't jump to line 1999 because the condition on line 1998 was never true

1999 self.hint_tester() 

2000 # run the upgrade test 

2001 else: 

2002 self.upgrade_testing() 

2003 

2004 self.logger.info("> Stats from the installability tester") 

2005 for stat in self._inst_tester.stats.stats(): 

2006 self.logger.info("> %s", stat) 

2007 else: 

2008 self.logger.info("Migration computation skipped as requested.") 

2009 if not self.options.dry_run: 2009 ↛ 2011line 2009 didn't jump to line 2011 because the condition on line 2009 was always true

2010 self._policy_engine.save_state(self) 

2011 logging.shutdown() 

2012 

2013 

2014if __name__ == "__main__": 2014 ↛ 2015line 2014 didn't jump to line 2015 because the condition on line 2014 was never true

2015 Britney().main()