Coverage for britney2/britney.py: 83%

773 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-06-28 21:36 +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 Iterator 

191from functools import reduce 

192from itertools import chain 

193from operator import attrgetter 

194from typing import TYPE_CHECKING, Any, Optional, cast 

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 str(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 "Missing Package field in paragraph %d (file %s)" 

722 % (no, faux_packages_file) 

723 ) 

724 pkg_name = sys.intern(pkg_name) 

725 version = sys.intern(get_field("Version", "1.0-1")) 

726 provides_raw = get_field("Provides") 

727 archs_raw = get_field("Architecture", None) 

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

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

730 archs = archs_raw.split() 

731 else: 

732 archs = self.options.architectures 

733 faux_section = "faux" 

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

735 faux_section = "%s/faux" % component 

736 src_data = SourcePackage( 

737 pkg_name, 

738 version, 

739 sys.intern(faux_section), 

740 set(), 

741 None, 

742 True, 

743 None, 

744 None, 

745 [], 

746 [], 

747 ) 

748 

749 target_suite.sources[pkg_name] = src_data 

750 pri_source_suite.sources[pkg_name] = src_data 

751 

752 for arch in archs: 

753 pkg_id = BinaryPackageId(pkg_name, version, arch) 

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

755 provides = parse_provides( 

756 provides_raw, pkg_id=pkg_id, logger=self.logger 

757 ) 

758 else: 

759 provides = None 

760 bin_data = BinaryPackage( 

761 faux_section, 

762 pkg_name, 

763 version, 

764 arch, 

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

766 None, 

767 None, 

768 provides, 

769 False, 

770 pkg_id, 

771 None, 

772 ) 

773 

774 src_data.binaries.add(pkg_id) 

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

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

777 

778 # register provided packages with the target suite provides table 

779 for provided_pkg, provided_version, _ in ( 779 ↛ 782line 779 didn't jump to line 782

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

781 ): 

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

783 (pkg_name, provided_version) 

784 ) 

785 

786 self.all_binaries[pkg_id] = bin_data 

787 

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

789 """Loads configurable constraints 

790 

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

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

793 installable". 

794 

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

796 """ 

797 tag_file = apt_pkg.TagFile(constraints_file) 

798 get_field = tag_file.section.get 

799 step = tag_file.step 

800 no = 0 

801 faux_version = sys.intern("1") 

802 faux_section = sys.intern("faux") 

803 keep_installable: list[str] = [] 

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

805 pri_source_suite = self.suite_info.primary_source_suite 

806 target_suite = self.suite_info.target_suite 

807 

808 while step(): 

809 no += 1 

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

811 if pkg_name is None: # pragma: no cover 

812 raise ValueError( 

813 "Missing Fake-Package-Name field in paragraph %d (file %s)" 

814 % (no, constraints_file) 

815 ) 

816 pkg_name = sys.intern(pkg_name) 

817 

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

819 v: str = get_field(x, None) 

820 if v is None: # pragma: no cover 

821 raise ValueError( 

822 "Missing %s field for %s (file %s)" 

823 % (x, pkg_name, constraints_file) 

824 ) 

825 return v 

826 

827 constraint = mandatory_field("Constraint") 

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

829 raise ValueError( 

830 "Unsupported constraint %s for %s (file %s)" 

831 % (constraint, pkg_name, constraints_file) 

832 ) 

833 

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

835 

836 pkg_list = [ 

837 x.strip() 

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

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

840 ] 

841 src_data = SourcePackage( 

842 pkg_name, 

843 faux_version, 

844 faux_section, 

845 set(), 

846 None, 

847 True, 

848 None, 

849 None, 

850 [], 

851 [], 

852 ) 

853 target_suite.sources[pkg_name] = src_data 

854 pri_source_suite.sources[pkg_name] = src_data 

855 keep_installable.append(pkg_name) 

856 for arch in self.options.architectures: 

857 deps = [] 

858 for pkg_spec in pkg_list: 

859 s = pkg_spec.split(None, 1) 

860 if len(s) == 1: 

861 deps.append(s[0]) 

862 else: 

863 pkg, arch_res = s 

864 if not ( 

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

866 ): # pragma: no cover 

867 raise ValueError( 

868 "Invalid arch-restriction on %s - should be [arch1 arch2] (for %s file %s)" 

869 % (pkg, pkg_name, constraints_file) 

870 ) 

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

872 if not arch_res_l: # pragma: no cover 

873 msg = "Empty arch-restriction for %s: Uses comma or negation (for %s file %s)" 

874 raise ValueError(msg % (pkg, pkg_name, constraints_file)) 

875 for a in arch_res_l: 

876 if a == arch: 

877 deps.append(pkg) 

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

879 msg = "Invalid arch-restriction for %s: Uses comma or negation (for %s file %s)" 

880 raise ValueError( 

881 msg % (pkg, pkg_name, constraints_file) 

882 ) 

883 pkg_id = BinaryPackageId(pkg_name, faux_version, arch) 

884 bin_data = BinaryPackage( 

885 faux_section, 

886 pkg_name, 

887 faux_version, 

888 arch, 

889 MultiArch.NO, 

890 ", ".join(deps), 

891 None, 

892 [], 

893 False, 

894 pkg_id, 

895 [], 

896 ) 

897 src_data.binaries.add(pkg_id) 

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

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

900 self.all_binaries[pkg_id] = bin_data 

901 

902 return constraints 

903 

904 # Data reading/writing methods 

905 # ---------------------------- 

906 

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

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

909 

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

911 specified by the `hintsdir' parameter. 

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

913 for the hints. 

914 

915 The file contains rows with the format: 

916 

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

918 

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

920 the value is the list of affected packages. 

921 """ 

922 

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

924 if who == "command-line": 

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

926 filename = "<cmd-line>" 

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

928 else: 

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

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

931 self.logger.error( 

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

933 ) 

934 continue 

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

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

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

938 

939 hints = self._hint_parser.hints 

940 

941 for x in ( 

942 "block", 

943 "block-all", 

944 "block-udeb", 

945 "unblock", 

946 "unblock-udeb", 

947 "force", 

948 "urgent", 

949 "remove", 

950 "age-days", 

951 ): 

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

953 for hint in hints[x]: 

954 package = hint.package 

955 architecture = hint.architecture 

956 key = (hint, hint.user) 

957 if ( 

958 package in z 

959 and architecture in z[package] 

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

961 ): 

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

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

964 assert hint.version is not None 

965 assert hint2.version is not None 

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

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

968 self.logger.warning( 

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

970 x, 

971 package, 

972 hint2.version, 

973 hint2.architecture, 

974 hint2.user, 

975 hint.version, 

976 hint.architecture, 

977 hint.user, 

978 ) 

979 hint2.set_active(False) 

980 else: 

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

982 self.logger.warning( 

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

984 x, 

985 package, 

986 hint.version, 

987 hint.architecture, 

988 hint.user, 

989 hint2.version, 

990 hint2.architecture, 

991 hint2.user, 

992 ) 

993 hint.set_active(False) 

994 else: 

995 self.logger.warning( 

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

997 x, 

998 package, 

999 hint2.user, 

1000 hint2, 

1001 hint.user, 

1002 hint, 

1003 ) 

1004 hint2.set_active(False) 

1005 

1006 z[package][architecture] = key 

1007 

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

1009 if hint.architecture == "source": 

1010 for arch in self.options.architectures: 

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

1012 else: 

1013 assert hint.architecture is not None 

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

1015 

1016 # Sanity check the hints hash 

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

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

1019 

1020 # Remove all hints that were set inactive. 

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

1022 hints.remove_inactive_hints() 

1023 

1024 def write_excuses(self) -> None: 

1025 """Produce and write the update excuses 

1026 

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

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

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

1030 """ 

1031 

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

1033 

1034 mi_factory = self._migration_item_factory 

1035 excusefinder = ExcuseFinder( 

1036 self.options, 

1037 self.suite_info, 

1038 self.all_binaries, 

1039 self.pkg_universe, 

1040 self._policy_engine, 

1041 mi_factory, 

1042 self.hints, 

1043 ) 

1044 

1045 excuses, upgrade_me = excusefinder.find_actionable_excuses() 

1046 self.excuses = excuses 

1047 

1048 # sort the list of candidates 

1049 self.upgrade_me = sorted(upgrade_me) 

1050 old_lib_removals = old_libraries( 

1051 mi_factory, self.suite_info, self.options.outofsync_arches 

1052 ) 

1053 self.upgrade_me.extend(old_lib_removals) 

1054 self.output_logger.info( 

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

1056 ) 

1057 log_and_format_old_libraries(self.output_logger, old_lib_removals) 

1058 

1059 # write excuses to the output file 

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

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

1062 write_excuses( 

1063 excuses, 

1064 self.options.excuses_output, 

1065 output_format=ExcusesOutputFormat.LEGACY_HTML, 

1066 ) 

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

1068 self.logger.info( 

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

1070 ) 

1071 write_excuses( 

1072 excuses, 

1073 self.options.excuses_yaml_output, 

1074 output_format=ExcusesOutputFormat.YAML, 

1075 ) 

1076 

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

1078 

1079 # Upgrade run 

1080 # ----------- 

1081 

1082 def eval_nuninst( 

1083 self, 

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

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

1086 ) -> str: 

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

1088 

1089 This method returns a string which represents the uninstallability 

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

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

1092 

1093 An example of the output string is: 

1094 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 

1095 

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

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

1098 architectures. 

1099 """ 

1100 res = [] 

1101 total = 0 

1102 totalbreak = 0 

1103 for arch in self.options.architectures: 

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

1105 n = len(nuninst[arch]) 

1106 elif original and arch in original: 

1107 n = len(original[arch]) 

1108 else: 

1109 continue 

1110 if arch in self.options.break_arches: 

1111 totalbreak = totalbreak + n 

1112 else: 

1113 total = total + n 

1114 res.append("%s-%d" % (arch[0], n)) 

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

1116 

1117 def iter_packages( 

1118 self, 

1119 packages: list[MigrationItem], 

1120 selected: list[MigrationItem], 

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

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

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

1124 

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

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

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

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

1129 

1130 :param selected: list of MigrationItem? 

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

1132 """ 

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

1134 group_info = {} 

1135 rescheduled_packages = packages 

1136 maybe_rescheduled_packages: list[MigrationItem] = [] 

1137 output_logger = self.output_logger 

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

1139 mm = self._migration_manager 

1140 target_suite = self.suite_info.target_suite 

1141 

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

1143 try: 

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

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

1146 group_info[y] = result 

1147 except MigrationConstraintException as e: 

1148 rescheduled_packages.remove(y) 

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

1150 output_logger.info(" got exception: %s" % (repr(e))) 

1151 

1152 if nuninst: 

1153 nuninst_orig = nuninst 

1154 else: 

1155 nuninst_orig = self.nuninst_orig 

1156 

1157 nuninst_last_accepted = nuninst_orig 

1158 

1159 output_logger.info( 

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

1161 ) 

1162 while rescheduled_packages: 

1163 groups = [group_info[x] for x in rescheduled_packages] 

1164 worklist = solver.solve_groups(groups) 

1165 rescheduled_packages = [] 

1166 

1167 worklist.reverse() 

1168 

1169 while worklist: 

1170 comp = worklist.pop() 

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

1172 output_logger.info("trying: %s" % comp_name) 

1173 with mm.start_transaction() as transaction: 

1174 accepted = False 

1175 try: 

1176 ( 

1177 accepted, 

1178 nuninst_after, 

1179 failed_arch, 

1180 new_cruft, 

1181 ) = mm.migrate_items_to_target_suite( 

1182 comp, nuninst_last_accepted 

1183 ) 

1184 if accepted: 

1185 selected.extend(comp) 

1186 transaction.commit() 

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

1188 output_logger.info( 

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

1190 ) 

1191 output_logger.info( 

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

1193 ) 

1194 output_logger.info( 

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

1196 ) 

1197 if new_cruft: 

1198 output_logger.info( 

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

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

1201 ) 

1202 

1203 if len(selected) <= 20: 

1204 output_logger.info( 

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

1206 ) 

1207 else: 

1208 output_logger.info( 

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

1210 len(selected), 

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

1212 ) 

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

1214 target_suite.check_suite_source_pkg_consistency( 

1215 "iter_packages after commit" 

1216 ) 

1217 nuninst_last_accepted = nuninst_after 

1218 for cruft_item in new_cruft: 

1219 try: 

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

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

1222 group_info[cruft_item] = result 

1223 worklist.append([cruft_item]) 

1224 except MigrationConstraintException as e: 

1225 output_logger.info( 

1226 " got exception adding cruft item %s to list: %s" 

1227 % (cruft_item.uvname, repr(e)) 

1228 ) 

1229 rescheduled_packages.extend(maybe_rescheduled_packages) 

1230 maybe_rescheduled_packages.clear() 

1231 else: 

1232 transaction.rollback() 

1233 assert failed_arch # type checking 

1234 broken = sorted( 

1235 b 

1236 for b in nuninst_after[failed_arch] 

1237 if b not in nuninst_last_accepted[failed_arch] 

1238 ) 

1239 compare_nuninst = None 

1240 if any( 

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

1242 ): 

1243 compare_nuninst = nuninst_last_accepted 

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

1245 output_logger.info( 

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

1247 comp_name, 

1248 len(rescheduled_packages), 

1249 len(maybe_rescheduled_packages), 

1250 len(worklist), 

1251 ) 

1252 output_logger.info( 

1253 " got: %s", 

1254 self.eval_nuninst(nuninst_after, compare_nuninst), 

1255 ) 

1256 output_logger.info( 

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

1258 ) 

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

1260 target_suite.check_suite_source_pkg_consistency( 

1261 "iter_package after rollback (not accepted)" 

1262 ) 

1263 

1264 except MigrationConstraintException as e: 

1265 transaction.rollback() 

1266 output_logger.info( 

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

1268 comp_name, 

1269 len(rescheduled_packages), 

1270 len(maybe_rescheduled_packages), 

1271 len(worklist), 

1272 ) 

1273 output_logger.info(" got exception: %s" % (repr(e))) 

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

1275 target_suite.check_suite_source_pkg_consistency( 

1276 "iter_package after rollback (MigrationConstraintException)" 

1277 ) 

1278 

1279 if not accepted: 

1280 if len(comp) > 1: 

1281 output_logger.info( 

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

1283 ) 

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

1285 else: 

1286 maybe_rescheduled_packages.append(comp[0]) 

1287 

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

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

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

1291 format_and_log_uninst( 

1292 output_logger, 

1293 self.options.architectures, 

1294 newly_uninst(self.nuninst_orig, nuninst_last_accepted), 

1295 ) 

1296 output_logger.info("") 

1297 

1298 return (nuninst_last_accepted, maybe_rescheduled_packages) 

1299 

1300 def do_all( 

1301 self, 

1302 hinttype: str | None = None, 

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

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

1305 ) -> None: 

1306 """Testing update runner 

1307 

1308 This method tries to update testing checking the uninstallability 

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

1310 successful or not. 

1311 """ 

1312 selected = [] 

1313 if actions: 

1314 upgrade_me = actions[:] 

1315 else: 

1316 upgrade_me = self.upgrade_me[:] 

1317 nuninst_start = self.nuninst_orig 

1318 output_logger = self.output_logger 

1319 target_suite = self.suite_info.target_suite 

1320 

1321 # these are special parameters for hints processing 

1322 force = False 

1323 recurse = True 

1324 nuninst_end = None 

1325 extra: list[MigrationItem] = [] 

1326 mm = self._migration_manager 

1327 

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

1329 force = hinttype == "force-hint" 

1330 recurse = False 

1331 

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

1333 if init: 

1334 for x in init: 

1335 if x not in upgrade_me: 

1336 output_logger.warning( 

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

1338 x.uvname, 

1339 ) 

1340 return None 

1341 selected.append(x) 

1342 upgrade_me.remove(x) 

1343 

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

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

1346 

1347 if not (init and not force): 

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

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

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

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

1352 

1353 @contextlib.contextmanager 

1354 def _start_transaction() -> Iterator[Optional["MigrationTransactionState"]]: 

1355 yield None 

1356 

1357 else: 

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

1359 _start_transaction = mm.start_transaction 

1360 

1361 with _start_transaction() as transaction: 

1362 if init: 

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

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

1365 selected, self.nuninst_orig, stop_on_first_regression=False 

1366 ) 

1367 

1368 if recurse: 

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

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

1371 selected_set = set(selected) 

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

1373 else: 

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

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

1376 if new_cruft: 

1377 output_logger.info( 

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

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

1380 ) 

1381 cruft.extend(new_cruft) 

1382 if cruft: 

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

1384 (nuninst_end, remaining_cruft) = self.iter_packages( 

1385 cruft, selected, nuninst=nuninst_end 

1386 ) 

1387 output_logger.info( 

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

1389 len(cruft) - len(remaining_cruft), 

1390 len(cruft), 

1391 ) 

1392 new_cruft.difference_update(remaining_cruft) 

1393 

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

1395 # them for us. 

1396 upgrade_me.extend(new_cruft) 

1397 

1398 if recurse: 

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

1400 (nuninst_end, extra) = self.iter_packages( 

1401 upgrade_me, selected, nuninst=nuninst_end 

1402 ) 

1403 

1404 assert nuninst_end is not None 

1405 nuninst_end_str = self.eval_nuninst(nuninst_end) 

1406 

1407 if not recurse: 

1408 # easy or force-hint 

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

1410 

1411 if not force: 

1412 format_and_log_uninst( 

1413 self.output_logger, 

1414 self.options.architectures, 

1415 newly_uninst(nuninst_start, nuninst_end), 

1416 ) 

1417 

1418 if force: 

1419 # Force implies "unconditionally better" 

1420 better = True 

1421 else: 

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

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

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

1425 # do not allow any regressions on these architectures. 

1426 # This usually only happens with hints 

1427 break_arches = set() 

1428 better = is_nuninst_asgood_generous( 

1429 self.constraints, 

1430 self.allow_uninst, 

1431 self.options.architectures, 

1432 self.nuninst_orig, 

1433 nuninst_end, 

1434 break_arches, 

1435 ) 

1436 

1437 if better: 

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

1439 output_logger.info( 

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

1441 ) 

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

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

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

1445 if force: 

1446 broken = newly_uninst(nuninst_start, nuninst_end) 

1447 if broken: 

1448 output_logger.warning("force breaks:") 

1449 format_and_log_uninst( 

1450 self.output_logger, 

1451 self.options.architectures, 

1452 broken, 

1453 loglevel=logging.WARNING, 

1454 ) 

1455 else: 

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

1457 output_logger.info( 

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

1459 ) 

1460 self.nuninst_orig = nuninst_end 

1461 self.all_selected += selected 

1462 if transaction: 

1463 transaction.commit() 

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

1465 target_suite.check_suite_source_pkg_consistency( 

1466 "do_all after commit" 

1467 ) 

1468 if not actions: 

1469 if recurse: 

1470 self.upgrade_me = extra 

1471 else: 

1472 selected_set = set(selected) 

1473 self.upgrade_me = [ 

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

1475 ] 

1476 else: 

1477 output_logger.info("FAILED\n") 

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

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

1480 # leave a broken state behind 

1481 # this should not happen 

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

1483 transaction.rollback() 

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

1485 target_suite.check_suite_source_pkg_consistency( 

1486 "do_all after rollback" 

1487 ) 

1488 

1489 output_logger.info("") 

1490 

1491 def assert_nuninst_is_correct(self) -> None: 

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

1493 

1494 cached_nuninst = self.nuninst_orig 

1495 self._inst_tester.compute_installability() 

1496 computed_nuninst = compile_nuninst( 

1497 self.suite_info.target_suite, 

1498 self.options.architectures, 

1499 self.options.nobreakall_arches, 

1500 ) 

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

1502 only_on_break_archs = True 

1503 msg_l = [ 

1504 "==================== NUNINST OUT OF SYNC =========================" 

1505 ] 

1506 for arch in self.options.architectures: 

1507 expected_nuninst = set(cached_nuninst[arch]) 

1508 actual_nuninst = set(computed_nuninst[arch]) 

1509 false_negatives = actual_nuninst - expected_nuninst 

1510 false_positives = expected_nuninst - actual_nuninst 

1511 # Britney does not quite work correctly with 

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

1513 if ( 

1514 false_negatives or false_positives 

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

1516 only_on_break_archs = False 

1517 if false_negatives: 

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

1519 if false_positives: 

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

1521 if false_negatives or false_positives: 

1522 msg_l.append( 

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

1524 ) 

1525 msg_l.append(msg_l[0]) 

1526 for msg in msg_l: 

1527 if only_on_break_archs: 

1528 self.logger.warning(msg) 

1529 else: 

1530 self.logger.error(msg) 

1531 if not only_on_break_archs: 

1532 raise AssertionError("NUNINST OUT OF SYNC") 

1533 else: 

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

1535 

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

1537 

1538 def upgrade_testing(self) -> None: 

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

1540 

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

1542 source suites. 

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

1544 commands. 

1545 """ 

1546 

1547 output_logger = self.output_logger 

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

1549 output_logger.info( 

1550 "Generated on: %s", 

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

1552 ) 

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

1554 

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

1556 # process `easy' hints 

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

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

1559 

1560 # process `force-hint' hints 

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

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

1563 

1564 # run the first round of the upgrade 

1565 # - do separate runs for break arches 

1566 allpackages = [] 

1567 normpackages = self.upgrade_me[:] 

1568 archpackages = {} 

1569 for a in self.options.break_arches: 

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

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

1572 self.upgrade_me = normpackages 

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

1574 self.do_all() 

1575 allpackages += self.upgrade_me 

1576 for a in self.options.break_arches: 

1577 backup = self.options.break_arches 

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

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

1580 ) 

1581 self.upgrade_me = archpackages[a] 

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

1583 self.do_all() 

1584 allpackages += self.upgrade_me 

1585 self.options.break_arches = backup 

1586 self.upgrade_me = allpackages 

1587 

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

1589 self.printuninstchange() 

1590 return 

1591 

1592 # process `hint' hints 

1593 hintcnt = 0 

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

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

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

1597 break 

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

1599 hintcnt += 1 

1600 

1601 # run the auto hinter 

1602 self.run_auto_hinter() 

1603 

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

1605 # obsolete source packages 

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

1607 # are built by it 

1608 self.logger.info( 

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

1610 ) 

1611 # local copies for performance 

1612 target_suite = self.suite_info.target_suite 

1613 sources_t = target_suite.sources 

1614 binaries_t = target_suite.binaries 

1615 mi_factory = self._migration_item_factory 

1616 used = { 

1617 binaries_t[arch][binary].source 

1618 for arch in binaries_t 

1619 for binary in binaries_t[arch] 

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

1621 } 

1622 removals = [ 

1623 mi_factory.parse_item( 

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

1625 ) 

1626 for source in sources_t 

1627 if source not in used 

1628 ] 

1629 if removals: 

1630 output_logger.info( 

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

1632 len(removals), 

1633 ) 

1634 self.do_all(actions=removals) 

1635 

1636 # smooth updates 

1637 removals = old_libraries( 

1638 self._migration_item_factory, self.suite_info, self.options.outofsync_arches 

1639 ) 

1640 if removals: 

1641 output_logger.info( 

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

1643 ) 

1644 log_and_format_old_libraries(self.output_logger, removals) 

1645 self.do_all(actions=removals) 

1646 removals = old_libraries( 

1647 self._migration_item_factory, 

1648 self.suite_info, 

1649 self.options.outofsync_arches, 

1650 ) 

1651 

1652 output_logger.info( 

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

1654 ) 

1655 log_and_format_old_libraries(self.output_logger, removals) 

1656 

1657 self.printuninstchange() 

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

1659 target_suite = self.suite_info.target_suite 

1660 self.assert_nuninst_is_correct() 

1661 target_suite.check_suite_source_pkg_consistency("end") 

1662 

1663 # output files 

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

1665 target_suite = self.suite_info.target_suite 

1666 

1667 # write HeidiResult 

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

1669 write_heidi( 

1670 self.options.heidi_output, 

1671 target_suite, 

1672 outofsync_arches=self.options.outofsync_arches, 

1673 ) 

1674 

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

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

1677 

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

1679 

1680 def printuninstchange(self) -> None: 

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

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

1683 

1684 if uninst: 

1685 self.output_logger.info("") 

1686 self.output_logger.info( 

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

1688 ) 

1689 format_and_log_uninst( 

1690 self.output_logger, 

1691 self.options.architectures, 

1692 uninst, 

1693 loglevel=logging.WARNING, 

1694 ) 

1695 

1696 def hint_tester(self) -> None: 

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

1698 

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

1700 try hints and evaluate the results. 

1701 """ 

1702 import readline 

1703 

1704 from britney2.completer import Completer 

1705 

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

1707 if os.path.exists(histfile): 

1708 readline.read_history_file(histfile) 

1709 

1710 readline.parse_and_bind("tab: complete") 

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

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

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

1714 readline.set_completer_delims( 

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

1716 ) 

1717 

1718 known_hints = self._hint_parser.registered_hint_names 

1719 

1720 print("Britney hint tester") 

1721 print() 

1722 print( 

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

1724 ) 

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

1726 print( 

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

1728 ) 

1729 print() 

1730 

1731 while True: 

1732 # read the command from the command line 

1733 try: 

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

1735 except EOFError: 

1736 print("") 

1737 break 

1738 except KeyboardInterrupt: 

1739 print("") 

1740 continue 

1741 match user_input: 

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

1743 # quit the hint tester 

1744 break 

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

1746 try: 

1747 import britney2.console 

1748 except ImportError as e: 

1749 print("Failed to import britney.console module: %s" % repr(e)) 

1750 continue 

1751 britney2.console.run_python_console(self) 

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

1753 # run a hint 

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

1755 mi_factory = self._migration_item_factory 

1756 try: 

1757 self.do_hint( 

1758 choice, 

1759 "hint-tester", 

1760 mi_factory.parse_items(items), 

1761 ) 

1762 self.printuninstchange() 

1763 except KeyboardInterrupt: 

1764 continue 

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

1766 self._hint_parser.parse_hints( 

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

1768 ) 

1769 self.write_excuses() 

1770 

1771 try: 

1772 readline.write_history_file(histfile) 

1773 except OSError as e: 

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

1775 

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

1777 """Process hints 

1778 

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

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

1781 is skipped. 

1782 """ 

1783 

1784 output_logger = self.output_logger 

1785 

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

1787 output_logger.info( 

1788 "Trying %s from %s: %s", 

1789 hinttype, 

1790 who, 

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

1792 ) 

1793 

1794 issues = [] 

1795 # loop on the requested packages and versions 

1796 for idx in range(len(pkgvers)): 

1797 pkg = pkgvers[idx] 

1798 # skip removal requests 

1799 if pkg.is_removal: 

1800 continue 

1801 

1802 suite = pkg.suite 

1803 

1804 assert pkg.version is not None 

1805 if pkg.package not in suite.sources: 1805 ↛ 1806line 1805 didn't jump to line 1806 because the condition on line 1805 was never true

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

1807 elif ( 1807 ↛ 1811line 1807 didn't jump to line 1811

1808 apt_pkg.version_compare(suite.sources[pkg.package].version, pkg.version) 

1809 != 0 

1810 ): 

1811 issues.append( 

1812 "Version mismatch, %s %s != %s" 

1813 % (pkg.package, pkg.version, suite.sources[pkg.package].version) 

1814 ) 

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

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

1817 return False 

1818 

1819 self.do_all(hinttype, pkgvers) 

1820 return True 

1821 

1822 def get_auto_hinter_hints( 

1823 self, upgrade_me: list[MigrationItem] 

1824 ) -> list[list[frozenset[MigrationItem]]]: 

1825 """Auto-generate "easy" hints. 

1826 

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

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

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

1830 reverse dependencies is recursively created. 

1831 

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

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

1834 then attempted in turn as "easy" hints. 

1835 

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

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

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

1839 """ 

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

1841 

1842 sources_t = self.suite_info.target_suite.sources 

1843 excuses = self.excuses 

1844 

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

1846 source = excuse.source 

1847 assert isinstance(excuse.item, MigrationItem) 

1848 arch = excuse.item.architecture 

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

1850 # migrated already 

1851 valid = ( 

1852 arch != "source" 

1853 or source not in sources_t 

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

1855 ) 

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

1857 # should not happen 

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

1859 raise AssertionError("excuse no longer valid %s" % (excuse.item)) 

1860 return valid 

1861 

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

1863 valid_excuses = frozenset( 

1864 e.name 

1865 for n, e in excuses.items() 

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

1867 ) 

1868 excuses_deps = { 

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

1870 for name, excuse in excuses.items() 

1871 if name in valid_excuses 

1872 } 

1873 excuses_rdeps = defaultdict(set) 

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

1875 for dep in deps: 

1876 excuses_rdeps[dep].add(name) 

1877 

1878 # loop on them 

1879 candidates = [] 

1880 mincands = [] 

1881 seen_hints = set() 

1882 for e in valid_excuses: 

1883 excuse = excuses[e] 

1884 if not excuse.get_deps(): 

1885 assert isinstance(excuse.item, MigrationItem) 

1886 items = [excuse.item] 

1887 orig_size = 1 

1888 looped = False 

1889 seen_items = set() 

1890 seen_items.update(items) 

1891 

1892 for item in items: 

1893 assert isinstance(item, MigrationItem) 

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

1895 new_items = cast( 

1896 set[MigrationItem], 

1897 { 

1898 excuses[x].item 

1899 for x in chain( 

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

1901 ) 

1902 }, 

1903 ) 

1904 new_items -= seen_items 

1905 items.extend(new_items) 

1906 seen_items.update(new_items) 

1907 

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

1909 orig_size = len(items) 

1910 h = frozenset(seen_items) 

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

1912 mincands.append(h) 

1913 seen_hints.add(h) 

1914 looped = True 

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

1916 h = frozenset(seen_items) 

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

1918 candidates.append(h) 

1919 seen_hints.add(h) 

1920 return [candidates, mincands] 

1921 

1922 def run_auto_hinter(self) -> None: 

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

1924 for hint in lst: 

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

1926 

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

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

1929 all = defaultdict(set) 

1930 binaries_t = self.suite_info.target_suite.binaries 

1931 for p in nuninst[arch]: 

1932 pkg = binaries_t[arch][p] 

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

1934 

1935 print("* %s" % arch) 

1936 

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

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

1939 

1940 print() 

1941 

1942 def _remove_archall_faux_packages(self) -> None: 

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

1944 

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

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

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

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

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

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

1951 remove them again. 

1952 

1953 """ 

1954 if not self.options.archall_inconsistency_allowed: 

1955 all_binaries = self.all_binaries 

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

1957 for pkg_a in faux_a: 

1958 del all_binaries[pkg_a] 

1959 

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

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

1962 binaries = suite.binaries[arch] 

1963 faux_b = { 

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

1965 } 

1966 for pkg_b in faux_b: 

1967 del binaries[pkg_b] 

1968 sources = suite.sources 

1969 for src in sources.keys(): 

1970 faux_s = { 

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

1972 } 

1973 sources[src].binaries -= faux_s 

1974 

1975 def main(self) -> None: 

1976 """Main method 

1977 

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

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

1980 """ 

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

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

1983 return 

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

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

1986 self.write_excuses() 

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

1988 else: 

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

1990 

1991 self._remove_archall_faux_packages() 

1992 

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

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

1995 self.logger.info( 

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

1997 " as this is a dry-run." 

1998 ) 

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

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

2001 file_handler = logging.FileHandler( 

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

2003 ) 

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

2005 file_handler.setFormatter(output_formatter) 

2006 self.output_logger.addHandler(file_handler) 

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

2008 else: 

2009 self.logger.info( 

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

2011 " as the UPGRADE_OUTPUT configuration is not provided." 

2012 ) 

2013 

2014 # run the hint tester 

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

2016 self.hint_tester() 

2017 # run the upgrade test 

2018 else: 

2019 self.upgrade_testing() 

2020 

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

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

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

2024 else: 

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

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

2027 self._policy_engine.save_state(self) 

2028 logging.shutdown() 

2029 

2030 

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

2032 Britney().main()