Coverage for dak/rm.py: 71%

174 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-01-04 16:18 +0000

1#! /usr/bin/env python3 

2 

3"""General purpose package removal tool for ftpmaster""" 

4# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006 James Troup <james@nocrew.org> 

5# Copyright (C) 2010 Alexander Reichle-Schmehl <tolimar@debian.org> 

6 

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

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

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

10# (at your option) any later version. 

11 

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

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

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

15# GNU General Public License for more details. 

16 

17# You should have received a copy of the GNU General Public License 

18# along with this program; if not, write to the Free Software 

19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

20 

21################################################################################ 

22 

23# o OpenBSD team wants to get changes incorporated into IPF. Darren no 

24# respond. 

25# o Ask again -> No respond. Darren coder supreme. 

26# o OpenBSD decide to make changes, but only in OpenBSD source 

27# tree. Darren hears, gets angry! Decides: "LICENSE NO ALLOW!" 

28# o Insert Flame War. 

29# o OpenBSD team decide to switch to different packet filter under BSD 

30# license. Because Project Goal: Every user should be able to make 

31# changes to source tree. IPF license bad!! 

32# o Darren try get back: says, NetBSD, FreeBSD allowed! MUAHAHAHAH!!! 

33# o Theo say: no care, pf much better than ipf! 

34# o Darren changes mind: changes license. But OpenBSD will not change 

35# back to ipf. Darren even much more bitter. 

36# o Darren so bitterbitter. Decides: I'LL GET BACK BY FORKING OPENBSD AND 

37# RELEASING MY OWN VERSION. HEHEHEHEHE. 

38 

39# http://slashdot.org/comments.pl?sid=26697&cid=2883271 

40 

41################################################################################ 

42 

43import functools 

44import sys 

45from collections import defaultdict 

46from typing import TYPE_CHECKING, NoReturn 

47 

48import apt_pkg 

49from sqlalchemy import sql 

50 

51from daklib import utils 

52from daklib.config import Config 

53from daklib.dbconn import DBConn, get_maintainer, get_suite 

54from daklib.rm import remove 

55 

56if TYPE_CHECKING: 

57 from sqlalchemy.orm import Session 

58 

59################################################################################ 

60 

61Options: apt_pkg.Configuration 

62 

63################################################################################ 

64 

65 

66def usage(exit_code=0) -> NoReturn: 

67 print( 

68 """Usage: dak rm [OPTIONS] PACKAGE[...] 

69Remove PACKAGE(s) from suite(s). 

70 

71 -A, --no-arch-all-rdeps Do not report breaking arch:all packages 

72 or Build-Depends-Indep 

73 -a, --architecture=ARCH only act on this architecture 

74 -b, --binary PACKAGE are binary packages to remove 

75 -B, --binary-only remove binaries only 

76 --binary-version=VER only remove packages with binary vesion VER 

77 -c, --component=COMPONENT act on this component 

78 -C, --carbon-copy=EMAIL send a CC of removal message to EMAIL 

79 -d, --done=BUG# send removal message as closure to bug# 

80 -D, --do-close also close all bugs associated to that package 

81 -h, --help show this help and exit 

82 -m, --reason=MSG reason for removal 

83 -n, --no-action don't do anything 

84 -o, --outdated remove only outdated sources or binaries that were 

85 built from previous source versions 

86 -p, --partial don't affect override files 

87 -R, --rdep-check check reverse dependencies 

88 -s, --suite=SUITE act on this suite 

89 -S, --source-only remove source only 

90 --source-version=VER only remove packages with source version VER 

91 

92ARCH, BUG#, COMPONENT and SUITE can be comma (or space) separated lists, e.g. 

93 --architecture=amd64,i386""" 

94 ) 

95 

96 sys.exit(exit_code) 

97 

98 

99################################################################################ 

100 

101# "Hudson: What that's great, that's just fucking great man, now what 

102# the fuck are we supposed to do? We're in some real pretty shit now 

103# man...That's it man, game over man, game over, man! Game over! What 

104# the fuck are we gonna do now? What are we gonna do?" 

105 

106 

107def game_over() -> None: 

108 answer = utils.input_or_exit("Continue (y/N)? ").lower() 

109 if answer != "y": 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true

110 print("Aborted.") 

111 sys.exit(1) 

112 

113 

114################################################################################ 

115 

116 

117def reverse_depends_check( 

118 removals: list[str], 

119 suite: str, 

120 arches: list[str] | None, 

121 session: "Session", 

122 include_arch_all: bool, 

123) -> None: 

124 print("Checking reverse dependencies...") 

125 if utils.check_reverse_depends( 125 ↛ 128line 125 didn't jump to line 128 because the condition on line 125 was never true

126 removals, suite, arches, session, include_arch_all=include_arch_all 

127 ): 

128 print("Dependency problem found.") 

129 if not Options["No-Action"]: 

130 game_over() 

131 else: 

132 print("No dependency problem found.") 

133 print() 

134 

135 

136################################################################################ 

137 

138 

139def main() -> None: 

140 global Options 

141 

142 cnf = Config() 

143 

144 Arguments = [ 

145 ("h", "help", "Rm::Options::Help"), 

146 ("A", "no-arch-all-rdeps", "Rm::Options::NoArchAllRdeps"), 

147 ("a", "architecture", "Rm::Options::Architecture", "HasArg"), 

148 ("b", "binary", "Rm::Options::Binary"), 

149 ("B", "binary-only", "Rm::Options::Binary-Only"), 

150 ("\0", "binary-version", "Rm::Options::Binary-Version", "HasArg"), 

151 ("c", "component", "Rm::Options::Component", "HasArg"), 

152 ("C", "carbon-copy", "Rm::Options::Carbon-Copy", "HasArg"), # Bugs to Cc 

153 ("d", "done", "Rm::Options::Done", "HasArg"), # Bugs fixed 

154 ("D", "do-close", "Rm::Options::Do-Close"), 

155 ("R", "rdep-check", "Rm::Options::Rdep-Check"), 

156 ( 

157 "m", 

158 "reason", 

159 "Rm::Options::Reason", 

160 "HasArg", 

161 ), # Hysterical raisins; -m is old-dinstall option for rejection reason 

162 ("n", "no-action", "Rm::Options::No-Action"), 

163 ("o", "outdated", "Rm::Options::Outdated"), 

164 ("p", "partial", "Rm::Options::Partial"), 

165 ("s", "suite", "Rm::Options::Suite", "HasArg"), 

166 ("S", "source-only", "Rm::Options::Source-Only"), 

167 ("\0", "source-version", "Rm::Options::Source-Version", "HasArg"), 

168 ] 

169 

170 for i in [ 

171 "NoArchAllRdeps", 

172 "architecture", 

173 "binary", 

174 "binary-only", 

175 "carbon-copy", 

176 "component", 

177 "done", 

178 "help", 

179 "no-action", 

180 "outdated", 

181 "partial", 

182 "rdep-check", 

183 "reason", 

184 "source-only", 

185 "Do-Close", 

186 ]: 

187 key = "Rm::Options::%s" % (i) 

188 if key not in cnf: 188 ↛ 170line 188 didn't jump to line 170

189 cnf[key] = "" 

190 if "Rm::Options::Suite" not in cnf: 190 ↛ 193line 190 didn't jump to line 193 because the condition on line 190 was always true

191 cnf["Rm::Options::Suite"] = "unstable" 

192 

193 arguments = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined] 

194 Options = cnf.subtree("Rm::Options") 

195 

196 if Options["Help"]: 

197 usage() 

198 

199 session = DBConn().session() 

200 

201 # Sanity check options 

202 if not arguments: 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true

203 utils.fubar("need at least one package name as an argument.") 

204 if Options["Architecture"] and Options["Source-Only"]: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true

205 utils.fubar( 

206 "can't use -a/--architecture and -S/--source-only options simultaneously." 

207 ) 

208 actions = [Options["Binary"], Options["Binary-Only"], Options["Source-Only"]] 

209 nr_actions = len([act for act in actions if act]) 

210 if nr_actions > 1: 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true

211 utils.fubar( 

212 "Only one of -b/--binary, -B/--binary-only and -S/--source-only can be used." 

213 ) 

214 if Options["Architecture"] and not Options["Partial"]: 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true

215 utils.warn("-a/--architecture implies -p/--partial.") 

216 Options["Partial"] = "true" # type: ignore[index] 

217 if Options["Outdated"] and not Options["Partial"]: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 utils.warn("-o/--outdated implies -p/--partial.") 

219 Options["Partial"] = "true" # type: ignore[index] 

220 if Options["Do-Close"] and not Options["Done"]: 220 ↛ 221line 220 didn't jump to line 221 because the condition on line 220 was never true

221 utils.fubar("-D/--do-close needs -d/--done (bugnr).") 

222 if Options["Do-Close"] and ( 222 ↛ 225line 222 didn't jump to line 225 because the condition on line 222 was never true

223 Options["Binary"] or Options["Binary-Only"] or Options["Source-Only"] 

224 ): 

225 utils.fubar( 

226 "-D/--do-close cannot be used with -b/--binary, -B/--binary-only or -S/--source-only." 

227 ) 

228 

229 # Force the admin to tell someone if we're not doing a 'dak 

230 # cruft-report' inspired removal (or closing a bug, which counts 

231 # as telling someone). 

232 if ( 232 ↛ 238line 232 didn't jump to line 238

233 not Options["No-Action"] 

234 and not Options["Carbon-Copy"] 

235 and not Options["Done"] 

236 and Options["Reason"].find("[auto-cruft]") == -1 

237 ): 

238 utils.fubar( 

239 "Need a -C/--carbon-copy if not closing a bug and not doing a cruft removal." 

240 ) 

241 

242 parameters: dict[str, object] = { 

243 "binary_version": Options.get("Binary-Version", "") or None, 

244 "source_version": Options.get("Source-Version", "") or None, 

245 } 

246 

247 if Options["Binary"]: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true

248 con_packages = "AND b.package IN :packages" 

249 parameters["packages"] = tuple(arguments) 

250 else: 

251 con_packages = "AND s.source IN :sources" 

252 parameters["sources"] = tuple(arguments) 

253 

254 (con_suites, con_architectures, con_components, check_source) = utils.parse_args( 

255 Options 

256 ) 

257 

258 # Additional suite checks 

259 suite_ids_list = [] 

260 whitelists = [] 

261 suites = utils.split_args(Options["Suite"]) 

262 suites_list = utils.join_with_commas_and(suites) 

263 if not Options["No-Action"]: 

264 for suite in suites: 

265 s = get_suite(suite, session=session) 

266 if s is not None: 266 ↛ 269line 266 didn't jump to line 269 because the condition on line 266 was always true

267 suite_ids_list.append(s.suite_id) 

268 whitelists.append(s.mail_whitelist) 

269 if suite in ("oldstable", "stable"): 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true

270 print("**WARNING** About to remove from the (old)stable suite!") 

271 print( 

272 "This should only be done just prior to a (point) release and not at" 

273 ) 

274 print("any other time.") 

275 game_over() 

276 elif suite == "testing": 276 ↛ 277line 276 didn't jump to line 277 because the condition on line 276 was never true

277 print("**WARNING About to remove from the testing suite!") 

278 print( 

279 "There's no need to do this normally as removals from unstable will" 

280 ) 

281 print("propogate to testing automagically.") 

282 game_over() 

283 

284 # Additional architecture checks 

285 if Options["Architecture"] and check_source: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true

286 utils.warn("'source' in -a/--argument makes no sense and is ignored.") 

287 

288 # Don't do dependency checks on multiple suites 

289 if Options["Rdep-Check"] and len(suites) > 1: 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true

290 utils.fubar("Reverse dependency check on multiple suites is not implemented.") 

291 

292 q_outdated = "TRUE" 

293 if Options["Outdated"]: 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true

294 q_outdated = "s.version < newest_source.version" 

295 

296 to_remove: list[tuple[str, str, str, int, int, str, str, str]] = [] 

297 maintainers: set[int] = set() 

298 

299 # We have 3 modes of package selection: binary, source-only, binary-only 

300 # and source+binary. 

301 

302 # XXX: TODO: This all needs converting to use placeholders or the object 

303 # API. It's an SQL injection dream at the moment 

304 

305 if Options["Binary"]: 305 ↛ 307line 305 didn't jump to line 307 because the condition on line 305 was never true

306 # Removal by binary package name 

307 q = session.execute( 

308 sql.text( 

309 """ 

310 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source, 

311 s.version as source_version, newest_source.version as newest_sversion 

312 FROM binaries b 

313 JOIN source s ON s.id = b.source 

314 JOIN bin_associations ba ON ba.bin = b.id 

315 JOIN architecture a ON a.id = b.architecture 

316 JOIN suite su ON su.id = ba.suite 

317 JOIN files f ON f.id = b.file 

318 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id 

319 JOIN component c ON c.id = af.component_id 

320 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite 

321 WHERE 

322 (:binary_version IS NULL OR b.version = :binary_version) 

323 AND (:source_version IS NULL OR s.version = :source_version) 

324 AND %s %s %s %s %s 

325 """ 

326 % ( 

327 q_outdated, 

328 con_packages, 

329 con_suites, 

330 con_components, 

331 con_architectures, 

332 ) 

333 ), 

334 parameters, 

335 ) 

336 to_remove.extend(q) # type: ignore[arg-type] 

337 else: 

338 # Source-only 

339 if not Options["Binary-Only"]: 339 ↛ 362line 339 didn't jump to line 362 because the condition on line 339 was always true

340 q = session.execute( 

341 sql.text( 

342 """ 

343 SELECT s.source, s.version, 'source', s.id, s.maintainer, s.source, 

344 s.version as source_version, newest_source.version as newest_sversion 

345 FROM source s 

346 JOIN src_associations sa ON sa.source = s.id 

347 JOIN suite su ON su.id = sa.suite 

348 JOIN archive ON archive.id = su.archive_id 

349 JOIN files f ON f.id = s.file 

350 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id 

351 JOIN component c ON c.id = af.component_id 

352 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite 

353 WHERE 

354 (:source_version IS NULL OR s.version = :source_version) 

355 AND %s %s %s %s 

356 """ 

357 % (q_outdated, con_packages, con_suites, con_components) 

358 ), 

359 parameters, 

360 ) 

361 to_remove.extend(q) # type: ignore[arg-type] 

362 if not Options["Source-Only"]: 362 ↛ 395line 362 didn't jump to line 395 because the condition on line 362 was always true

363 # Source + Binary 

364 q = session.execute( 

365 sql.text( 

366 """ 

367 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source, 

368 s.version as source_version, newest_source.version as newest_sversion 

369 FROM binaries b 

370 JOIN bin_associations ba ON b.id = ba.bin 

371 JOIN architecture a ON b.architecture = a.id 

372 JOIN suite su ON ba.suite = su.id 

373 JOIN archive ON archive.id = su.archive_id 

374 JOIN files_archive_map af ON b.file = af.file_id AND af.archive_id = archive.id 

375 JOIN component c ON af.component_id = c.id 

376 JOIN source s ON b.source = s.id 

377 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite 

378 WHERE 

379 (:binary_version IS NULL OR b.version = :binary_version) 

380 AND (:source_version IS NULL OR s.version = :source_version) 

381 AND %s %s %s %s %s 

382 """ 

383 % ( 

384 q_outdated, 

385 con_packages, 

386 con_suites, 

387 con_components, 

388 con_architectures, 

389 ) 

390 ), 

391 parameters, 

392 ) 

393 to_remove.extend(q) # type: ignore[arg-type] 

394 

395 if not to_remove: 395 ↛ 396line 395 didn't jump to line 396 because the condition on line 395 was never true

396 print("Nothing to do.") 

397 sys.exit(0) 

398 

399 # Process -C/--carbon-copy 

400 # 

401 # Accept 3 types of arguments (space separated): 

402 # 1) a number - assumed to be a bug number, i.e. nnnnn@bugs.debian.org 

403 # 2) the keyword 'package' - cc's $package@packages.debian.org for every argument 

404 # 3) contains a '@' - assumed to be an email address, used unmodified 

405 # 

406 carbon_copy = [] 

407 for copy_to in utils.split_args(Options.get("Carbon-Copy", "")): 

408 if copy_to.isdigit(): 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true

409 if "Dinstall::BugServer" in cnf: 

410 carbon_copy.append(copy_to + "@" + cnf["Dinstall::BugServer"]) 

411 else: 

412 utils.fubar( 

413 "Asked to send mail to #%s in BTS but Dinstall::BugServer is not configured" 

414 % copy_to 

415 ) 

416 elif copy_to == "package": 416 ↛ 417line 416 didn't jump to line 417 because the condition on line 416 was never true

417 for package in set([s[5] for s in to_remove]): 

418 if "Dinstall::PackagesServer" in cnf: 

419 carbon_copy.append(package + "@" + cnf["Dinstall::PackagesServer"]) 

420 elif "@" in copy_to: 420 ↛ 423line 420 didn't jump to line 423 because the condition on line 420 was always true

421 carbon_copy.append(copy_to) 

422 else: 

423 utils.fubar( 

424 "Invalid -C/--carbon-copy argument '%s'; not a bug number, 'package' or email address." 

425 % (copy_to) 

426 ) 

427 

428 # If we don't have a reason; spawn an editor so the user can add one 

429 # Write the rejection email out as the <foo>.reason file 

430 if not Options["Reason"] and not Options["No-Action"]: 430 ↛ 431line 430 didn't jump to line 431 because the condition on line 430 was never true

431 Options["Reason"] = utils.call_editor() # type: ignore[index] 

432 

433 # Generate the summary of what's to be removed 

434 d: dict[str, dict[str, list[str]]] = defaultdict(lambda: defaultdict(list)) 

435 for j in to_remove: 

436 package = j[0] 

437 version = j[1] 

438 architecture = j[2] 

439 maintainer = j[4] 

440 maintainers.add(maintainer) 

441 # source = j[5] 

442 # source_version = j[6] 

443 # source_newest = j[7] 

444 if architecture not in d[package][version]: 444 ↛ 435line 444 didn't jump to line 435 because the condition on line 444 was always true

445 d[package][version].append(architecture) 

446 

447 maintainer_list = [] 

448 for maintainer_id in maintainers: 

449 m = get_maintainer(maintainer_id, session=session) 

450 assert m is not None 

451 maintainer_list.append(m.name) 

452 summary = "" 

453 removals = sorted(d) 

454 for package in removals: 

455 versions = sorted(d[package], key=functools.cmp_to_key(apt_pkg.version_compare)) 

456 for version in versions: 

457 d[package][version].sort(key=utils.ArchKey) 

458 summary += "%10s | %10s | %s\n" % ( 

459 package, 

460 version, 

461 ", ".join(d[package][version]), 

462 ) 

463 print("Will remove the following packages from %s:" % (suites_list)) 

464 print() 

465 print(summary) 

466 print("Maintainer: %s" % ", ".join(maintainer_list)) 

467 if Options["Done"]: 

468 print("Will also close bugs: " + Options["Done"]) 

469 if carbon_copy: 

470 print("Will also send CCs to: " + ", ".join(carbon_copy)) 

471 if Options["Do-Close"]: 471 ↛ 472line 471 didn't jump to line 472 because the condition on line 471 was never true

472 print("Will also close associated bug reports.") 

473 print() 

474 print("------------------- Reason -------------------") 

475 print(Options["Reason"]) 

476 print("----------------------------------------------") 

477 print() 

478 

479 if Options["Rdep-Check"]: 

480 arches = utils.split_args(Options["Architecture"]) 

481 include_arch_all = Options["NoArchAllRdeps"] == "" 

482 check_all_arches = include_arch_all and "all" in arches 

483 # when arches is None, rdeps are checked on all arches in the suite 

484 reverse_depends_check( 

485 removals, 

486 suites[0], 

487 arches if not check_all_arches else None, 

488 session, 

489 include_arch_all=include_arch_all, 

490 ) 

491 

492 # If -n/--no-action, drop out here 

493 if Options["No-Action"]: 

494 sys.exit(0) 

495 

496 print("Going to remove the packages now.") 

497 game_over() 

498 

499 # Do the actual deletion 

500 print("Deleting...", end=" ") 

501 sys.stdout.flush() 

502 

503 bugs = utils.split_args(Options["Done"]) 

504 remove( 

505 session, 

506 Options["Reason"], 

507 suites, 

508 [j[0:4] for j in to_remove], 

509 partial=bool(Options["Partial"]), 

510 components=utils.split_args(Options["Component"]), 

511 done_bugs=bugs, 

512 carbon_copy=carbon_copy, 

513 close_related_bugs=bool(Options["Do-Close"]), 

514 ) 

515 

516 print("done.") 

517 

518 

519####################################################################################### 

520 

521 

522if __name__ == "__main__": 

523 main()