Coverage for dak/rm.py: 71%
174 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-01-04 16:18 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-01-04 16:18 +0000
1#! /usr/bin/env python3
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>
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.
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.
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
21################################################################################
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.
39# http://slashdot.org/comments.pl?sid=26697&cid=2883271
41################################################################################
43import functools
44import sys
45from collections import defaultdict
46from typing import TYPE_CHECKING, NoReturn
48import apt_pkg
49from sqlalchemy import sql
51from daklib import utils
52from daklib.config import Config
53from daklib.dbconn import DBConn, get_maintainer, get_suite
54from daklib.rm import remove
56if TYPE_CHECKING:
57 from sqlalchemy.orm import Session
59################################################################################
61Options: apt_pkg.Configuration
63################################################################################
66def usage(exit_code=0) -> NoReturn:
67 print(
68 """Usage: dak rm [OPTIONS] PACKAGE[...]
69Remove PACKAGE(s) from suite(s).
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
92ARCH, BUG#, COMPONENT and SUITE can be comma (or space) separated lists, e.g.
93 --architecture=amd64,i386"""
94 )
96 sys.exit(exit_code)
99################################################################################
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?"
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)
114################################################################################
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()
136################################################################################
139def main() -> None:
140 global Options
142 cnf = Config()
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 ]
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"
193 arguments = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
194 Options = cnf.subtree("Rm::Options")
196 if Options["Help"]:
197 usage()
199 session = DBConn().session()
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 )
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 )
242 parameters: dict[str, object] = {
243 "binary_version": Options.get("Binary-Version", "") or None,
244 "source_version": Options.get("Source-Version", "") or None,
245 }
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)
254 (con_suites, con_architectures, con_components, check_source) = utils.parse_args(
255 Options
256 )
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()
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.")
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.")
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"
296 to_remove: list[tuple[str, str, str, int, int, str, str, str]] = []
297 maintainers: set[int] = set()
299 # We have 3 modes of package selection: binary, source-only, binary-only
300 # and source+binary.
302 # XXX: TODO: This all needs converting to use placeholders or the object
303 # API. It's an SQL injection dream at the moment
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]
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)
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 )
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]
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)
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()
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 )
492 # If -n/--no-action, drop out here
493 if Options["No-Action"]:
494 sys.exit(0)
496 print("Going to remove the packages now.")
497 game_over()
499 # Do the actual deletion
500 print("Deleting...", end=" ")
501 sys.stdout.flush()
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 )
516 print("done.")
519#######################################################################################
522if __name__ == "__main__":
523 main()