Coverage for dak/rm.py: 71%
174 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
1"""General purpose package removal tool for ftpmaster"""
3# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006 James Troup <james@nocrew.org>
4# Copyright (C) 2010 Alexander Reichle-Schmehl <tolimar@debian.org>
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20################################################################################
22# o OpenBSD team wants to get changes incorporated into IPF. Darren no
23# respond.
24# o Ask again -> No respond. Darren coder supreme.
25# o OpenBSD decide to make changes, but only in OpenBSD source
26# tree. Darren hears, gets angry! Decides: "LICENSE NO ALLOW!"
27# o Insert Flame War.
28# o OpenBSD team decide to switch to different packet filter under BSD
29# license. Because Project Goal: Every user should be able to make
30# changes to source tree. IPF license bad!!
31# o Darren try get back: says, NetBSD, FreeBSD allowed! MUAHAHAHAH!!!
32# o Theo say: no care, pf much better than ipf!
33# o Darren changes mind: changes license. But OpenBSD will not change
34# back to ipf. Darren even much more bitter.
35# o Darren so bitterbitter. Decides: I'LL GET BACK BY FORKING OPENBSD AND
36# RELEASING MY OWN VERSION. HEHEHEHEHE.
38# http://slashdot.org/comments.pl?sid=26697&cid=2883271
40################################################################################
42import functools
43import sys
44from collections import defaultdict
45from typing import TYPE_CHECKING, NoReturn
47import apt_pkg
48from sqlalchemy import sql
50from daklib import utils
51from daklib.config import Config
52from daklib.dbconn import DBConn, get_maintainer, get_suite
53from daklib.rm import remove
55if TYPE_CHECKING:
56 from sqlalchemy.orm import Session
58################################################################################
60Options: apt_pkg.Configuration
62################################################################################
65def usage(exit_code=0) -> NoReturn:
66 print(
67 """Usage: dak rm [OPTIONS] PACKAGE[...]
68Remove PACKAGE(s) from suite(s).
70 -A, --no-arch-all-rdeps Do not report breaking arch:all packages
71 or Build-Depends-Indep
72 -a, --architecture=ARCH only act on this architecture
73 -b, --binary PACKAGE are binary packages to remove
74 -B, --binary-only remove binaries only
75 --binary-version=VER only remove packages with binary version VER
76 (can be a comma-separated list)
77 --keep-binary-version=VER
78 never remove packages with binary version VER
79 (can be a comma-separated list)
80 -c, --component=COMPONENT act on this component
81 -C, --carbon-copy=EMAIL send a CC of removal message to EMAIL
82 -d, --done=BUG# send removal message as closure to bug#
83 -D, --do-close also close all bugs associated to that package
84 -h, --help show this help and exit
85 -m, --reason=MSG reason for removal
86 -n, --no-action don't do anything
87 -o, --outdated remove only outdated sources or binaries that were
88 built from previous source versions
89 -p, --partial don't affect override files
90 -R, --rdep-check check reverse dependencies
91 -s, --suite=SUITE act on this suite
92 -S, --source-only remove source only
93 --source-version=VER only remove packages with source version VER
94 (can be a comma-separated list)
95 --keep-source-version=VER
96 never remove packages with source version VER
97 (can be a comma-separated list)
99ARCH, BUG#, COMPONENT, SUITE and VER can be comma (or space) separated lists, e.g.
100 --architecture=amd64,i386"""
101 )
103 sys.exit(exit_code)
106################################################################################
108# "Hudson: What that's great, that's just fucking great man, now what
109# the fuck are we supposed to do? We're in some real pretty shit now
110# man...That's it man, game over man, game over, man! Game over! What
111# the fuck are we gonna do now? What are we gonna do?"
114def game_over() -> None:
115 answer = utils.input_or_exit("Continue (y/N)? ").lower()
116 if answer != "y": 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 print("Aborted.")
118 sys.exit(1)
121################################################################################
124def reverse_depends_check(
125 removals: list[str],
126 suite: str,
127 arches: list[str] | None,
128 session: "Session",
129 include_arch_all: bool,
130) -> None:
131 print("Checking reverse dependencies...")
132 if utils.check_reverse_depends( 132 ↛ 135line 132 didn't jump to line 135 because the condition on line 132 was never true
133 removals, suite, arches, session, include_arch_all=include_arch_all
134 ):
135 print("Dependency problem found.")
136 if not Options["No-Action"]:
137 game_over()
138 else:
139 print("No dependency problem found.")
140 print()
143################################################################################
146def main() -> None:
147 global Options
149 cnf = Config()
151 Arguments = [
152 ("h", "help", "Rm::Options::Help"),
153 ("A", "no-arch-all-rdeps", "Rm::Options::NoArchAllRdeps"),
154 ("a", "architecture", "Rm::Options::Architecture", "HasArg"),
155 ("b", "binary", "Rm::Options::Binary"),
156 ("B", "binary-only", "Rm::Options::Binary-Only"),
157 ("\0", "binary-version", "Rm::Options::Binary-Version", "HasArg"),
158 ("\0", "keep-binary-version", "Rm::Options::Keep-Binary-Version", "HasArg"),
159 ("c", "component", "Rm::Options::Component", "HasArg"),
160 ("C", "carbon-copy", "Rm::Options::Carbon-Copy", "HasArg"), # Bugs to Cc
161 ("d", "done", "Rm::Options::Done", "HasArg"), # Bugs fixed
162 ("D", "do-close", "Rm::Options::Do-Close"),
163 ("R", "rdep-check", "Rm::Options::Rdep-Check"),
164 (
165 "m",
166 "reason",
167 "Rm::Options::Reason",
168 "HasArg",
169 ), # Hysterical raisins; -m is old-dinstall option for rejection reason
170 ("n", "no-action", "Rm::Options::No-Action"),
171 ("o", "outdated", "Rm::Options::Outdated"),
172 ("p", "partial", "Rm::Options::Partial"),
173 ("s", "suite", "Rm::Options::Suite", "HasArg"),
174 ("S", "source-only", "Rm::Options::Source-Only"),
175 ("\0", "source-version", "Rm::Options::Source-Version", "HasArg"),
176 ("\0", "keep-source-version", "Rm::Options::Keep-Source-Version", "HasArg"),
177 ]
179 for i in [
180 "NoArchAllRdeps",
181 "architecture",
182 "binary",
183 "binary-only",
184 "carbon-copy",
185 "component",
186 "done",
187 "help",
188 "no-action",
189 "outdated",
190 "partial",
191 "rdep-check",
192 "reason",
193 "source-only",
194 "Do-Close",
195 ]:
196 key = "Rm::Options::%s" % (i)
197 if key not in cnf: 197 ↛ 179line 197 didn't jump to line 179
198 cnf[key] = ""
199 if "Rm::Options::Suite" not in cnf: 199 ↛ 202line 199 didn't jump to line 202 because the condition on line 199 was always true
200 cnf["Rm::Options::Suite"] = "unstable"
202 arguments = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
203 Options = cnf.subtree("Rm::Options")
205 if Options["Help"]:
206 usage()
208 session = DBConn().session()
210 # Sanity check options
211 if not arguments: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 utils.fubar("need at least one package name as an argument.")
213 if Options["Architecture"] and Options["Source-Only"]: 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true
214 utils.fubar(
215 "can't use -a/--architecture and -S/--source-only options simultaneously."
216 )
217 actions = [Options["Binary"], Options["Binary-Only"], Options["Source-Only"]]
218 nr_actions = len([act for act in actions if act])
219 if nr_actions > 1: 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 utils.fubar(
221 "Only one of -b/--binary, -B/--binary-only and -S/--source-only can be used."
222 )
223 if Options["Architecture"] and not Options["Partial"]: 223 ↛ 224line 223 didn't jump to line 224 because the condition on line 223 was never true
224 utils.warn("-a/--architecture implies -p/--partial.")
225 Options["Partial"] = "true" # type: ignore[index]
226 if Options["Outdated"] and not Options["Partial"]: 226 ↛ 227line 226 didn't jump to line 227 because the condition on line 226 was never true
227 utils.warn("-o/--outdated implies -p/--partial.")
228 Options["Partial"] = "true" # type: ignore[index]
229 if Options["Do-Close"] and not Options["Done"]: 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true
230 utils.fubar("-D/--do-close needs -d/--done (bugnr).")
231 if Options["Do-Close"] and ( 231 ↛ 234line 231 didn't jump to line 234 because the condition on line 231 was never true
232 Options["Binary"] or Options["Binary-Only"] or Options["Source-Only"]
233 ):
234 utils.fubar(
235 "-D/--do-close cannot be used with -b/--binary, -B/--binary-only or -S/--source-only."
236 )
238 # Force the admin to tell someone if we're not doing a 'dak
239 # cruft-report' inspired removal (or closing a bug, which counts
240 # as telling someone).
241 if ( 241 ↛ 248line 241 didn't jump to line 248
242 not Options["No-Action"]
243 and not Options["Carbon-Copy"]
244 and not Options["Done"]
245 and Options["Reason"].find("[auto-cruft]") == -1
246 and Options["Reason"].find("[cruft]") == -1
247 ):
248 utils.fubar(
249 "Need a -C/--carbon-copy if not closing a bug and not doing a cruft removal."
250 )
252 parameters: dict[str, object] = {
253 "binary_versions": utils.split_args_or_none(Options.get("Binary-Version", "")),
254 "source_versions": utils.split_args_or_none(Options.get("Source-Version", "")),
255 "keep_binary_versions": utils.split_args_or_none(
256 Options.get("Keep-Binary-Version", "")
257 ),
258 "keep_source_versions": utils.split_args_or_none(
259 Options.get("Keep-Source-Version", "")
260 ),
261 }
263 if Options["Binary"]: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true
264 con_packages = "AND b.package IN :packages"
265 parameters["packages"] = tuple(arguments)
266 else:
267 con_packages = "AND s.source IN :sources"
268 parameters["sources"] = tuple(arguments)
270 (con_suites, con_architectures, con_components, check_source) = utils.parse_args(
271 Options
272 )
274 # Additional suite checks
275 suite_ids_list = []
276 whitelists = []
277 suites = utils.split_args(Options["Suite"])
278 suites_list = utils.join_with_commas_and(suites)
279 if not Options["No-Action"]:
280 for suite in suites:
281 s = get_suite(suite, session=session)
282 if s is not None: 282 ↛ 285line 282 didn't jump to line 285 because the condition on line 282 was always true
283 suite_ids_list.append(s.suite_id)
284 whitelists.append(s.mail_whitelist)
285 if suite in ("oldstable", "stable"): 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 print("**WARNING** About to remove from the (old)stable suite!")
287 print(
288 "This should only be done just prior to a (point) release and not at"
289 )
290 print("any other time.")
291 game_over()
292 elif suite == "testing": 292 ↛ 293line 292 didn't jump to line 293 because the condition on line 292 was never true
293 print("**WARNING About to remove from the testing suite!")
294 print(
295 "There's no need to do this normally as removals from unstable will"
296 )
297 print("propogate to testing automagically.")
298 game_over()
300 # Additional architecture checks
301 if Options["Architecture"] and check_source: 301 ↛ 302line 301 didn't jump to line 302 because the condition on line 301 was never true
302 utils.warn("'source' in -a/--argument makes no sense and is ignored.")
304 # Don't do dependency checks on multiple suites
305 if Options["Rdep-Check"] and len(suites) > 1: 305 ↛ 306line 305 didn't jump to line 306 because the condition on line 305 was never true
306 utils.fubar("Reverse dependency check on multiple suites is not implemented.")
308 q_outdated = "TRUE"
309 if Options["Outdated"]: 309 ↛ 310line 309 didn't jump to line 310 because the condition on line 309 was never true
310 q_outdated = "s.version < newest_source.version"
312 to_remove: list[tuple[str, str, str, int, int, str, str, str]] = []
313 maintainers: set[int] = set()
315 # We have 3 modes of package selection: binary, source-only, binary-only
316 # and source+binary.
318 # XXX: TODO: This all needs converting to use placeholders or the object
319 # API. It's an SQL injection dream at the moment
321 if Options["Binary"]: 321 ↛ 323line 321 didn't jump to line 323 because the condition on line 321 was never true
322 # Removal by binary package name
323 q = session.execute(
324 sql.text(
325 """
326 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source,
327 s.version as source_version, newest_source.version as newest_sversion
328 FROM binaries b
329 JOIN source s ON s.id = b.source
330 JOIN bin_associations ba ON ba.bin = b.id
331 JOIN architecture a ON a.id = b.architecture
332 JOIN suite su ON su.id = ba.suite
333 JOIN files f ON f.id = b.file
334 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id
335 JOIN component c ON c.id = af.component_id
336 LEFT JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite
337 WHERE
338 (:binary_versions IS NULL OR b.version = ANY(CAST(:binary_versions AS debversion[])))
339 AND (:source_versions IS NULL OR s.version = ANY(CAST(:source_versions AS debversion[])))
340 AND (:keep_binary_versions IS NULL OR b.version != ALL(CAST(:keep_binary_versions AS debversion[])))
341 AND (:keep_source_versions IS NULL OR s.version != ALL(CAST(:keep_source_versions AS debversion[])))
342 AND %s %s %s %s %s
343 """
344 % (
345 q_outdated,
346 con_packages,
347 con_suites,
348 con_components,
349 con_architectures,
350 )
351 ),
352 parameters,
353 )
354 to_remove.extend(q) # type: ignore[arg-type]
355 else:
356 # Source-only
357 if not Options["Binary-Only"]: 357 ↛ 381line 357 didn't jump to line 381 because the condition on line 357 was always true
358 q = session.execute(
359 sql.text(
360 """
361 SELECT s.source, s.version, 'source', s.id, s.maintainer, s.source,
362 s.version as source_version, newest_source.version as newest_sversion
363 FROM source s
364 JOIN src_associations sa ON sa.source = s.id
365 JOIN suite su ON su.id = sa.suite
366 JOIN archive ON archive.id = su.archive_id
367 JOIN files f ON f.id = s.file
368 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id
369 JOIN component c ON c.id = af.component_id
370 LEFT JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite
371 WHERE
372 (:source_versions IS NULL OR s.version = ANY(CAST(:source_versions AS debversion[])))
373 AND (:keep_source_versions IS NULL OR s.version != ALL(CAST(:keep_source_versions AS debversion[])))
374 AND %s %s %s %s
375 """
376 % (q_outdated, con_packages, con_suites, con_components)
377 ),
378 parameters,
379 )
380 to_remove.extend(q) # type: ignore[arg-type]
381 if not Options["Source-Only"]: 381 ↛ 416line 381 didn't jump to line 416 because the condition on line 381 was always true
382 # Source + Binary
383 q = session.execute(
384 sql.text(
385 """
386 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source,
387 s.version as source_version, newest_source.version as newest_sversion
388 FROM binaries b
389 JOIN bin_associations ba ON b.id = ba.bin
390 JOIN architecture a ON b.architecture = a.id
391 JOIN suite su ON ba.suite = su.id
392 JOIN archive ON archive.id = su.archive_id
393 JOIN files_archive_map af ON b.file = af.file_id AND af.archive_id = archive.id
394 JOIN component c ON af.component_id = c.id
395 JOIN source s ON b.source = s.id
396 LEFT JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite
397 WHERE
398 (:binary_versions IS NULL OR b.version = ANY(CAST(:binary_versions AS debversion[])))
399 AND (:source_versions IS NULL OR s.version = ANY(CAST(:source_versions AS debversion[])))
400 AND (:keep_binary_versions IS NULL OR b.version != ALL(CAST(:keep_binary_versions AS debversion[])))
401 AND (:keep_source_versions IS NULL OR s.version != ALL(CAST(:keep_source_versions AS debversion[])))
402 AND %s %s %s %s %s
403 """
404 % (
405 q_outdated,
406 con_packages,
407 con_suites,
408 con_components,
409 con_architectures,
410 )
411 ),
412 parameters,
413 )
414 to_remove.extend(q) # type: ignore[arg-type]
416 if not to_remove: 416 ↛ 417line 416 didn't jump to line 417 because the condition on line 416 was never true
417 print("Nothing to do.")
418 sys.exit(0)
420 # Process -C/--carbon-copy
421 #
422 # Accept 3 types of arguments (space separated):
423 # 1) a number - assumed to be a bug number, i.e. nnnnn@bugs.debian.org
424 # 2) the keyword 'package' - cc's $package@packages.debian.org for every argument
425 # 3) contains a '@' - assumed to be an email address, used unmodified
426 #
427 carbon_copy = []
428 for copy_to in utils.split_args(Options.get("Carbon-Copy", "")):
429 if copy_to.isdigit(): 429 ↛ 430line 429 didn't jump to line 430 because the condition on line 429 was never true
430 if "Dinstall::BugServer" in cnf:
431 carbon_copy.append(copy_to + "@" + cnf["Dinstall::BugServer"])
432 else:
433 utils.fubar(
434 "Asked to send mail to #%s in BTS but Dinstall::BugServer is not configured"
435 % copy_to
436 )
437 elif copy_to == "package": 437 ↛ 438line 437 didn't jump to line 438 because the condition on line 437 was never true
438 for package in {s[5] for s in to_remove}:
439 if "Dinstall::PackagesServer" in cnf:
440 carbon_copy.append(package + "@" + cnf["Dinstall::PackagesServer"])
441 elif "@" in copy_to: 441 ↛ 444line 441 didn't jump to line 444 because the condition on line 441 was always true
442 carbon_copy.append(copy_to)
443 else:
444 utils.fubar(
445 "Invalid -C/--carbon-copy argument '%s'; not a bug number, 'package' or email address."
446 % (copy_to)
447 )
449 # If we don't have a reason; spawn an editor so the user can add one
450 # Write the rejection email out as the <foo>.reason file
451 if not Options["Reason"] and not Options["No-Action"]: 451 ↛ 452line 451 didn't jump to line 452 because the condition on line 451 was never true
452 Options["Reason"] = utils.call_editor() # type: ignore[index]
454 # Generate the summary of what's to be removed
455 d: dict[str, dict[str, list[str]]] = defaultdict(lambda: defaultdict(list))
456 for j in to_remove:
457 package = j[0]
458 version = j[1]
459 architecture = j[2]
460 maintainer = j[4]
461 maintainers.add(maintainer)
462 # source = j[5]
463 # source_version = j[6]
464 # source_newest = j[7]
465 if architecture not in d[package][version]: 465 ↛ 456line 465 didn't jump to line 456 because the condition on line 465 was always true
466 d[package][version].append(architecture)
468 maintainer_list = []
469 for maintainer_id in maintainers:
470 m = get_maintainer(maintainer_id, session=session)
471 assert m is not None
472 maintainer_list.append(m.name)
473 summary = ""
474 removals = sorted(d)
475 for package in removals:
476 versions = sorted(d[package], key=functools.cmp_to_key(apt_pkg.version_compare))
477 for version in versions:
478 d[package][version].sort(key=utils.ArchKey)
479 summary += "%10s | %10s | %s\n" % (
480 package,
481 version,
482 ", ".join(d[package][version]),
483 )
484 print("Will remove the following packages from %s:" % (suites_list))
485 print()
486 print(summary)
487 print("Maintainer: %s" % ", ".join(maintainer_list))
488 if Options["Done"]:
489 print("Will also close bugs: " + Options["Done"])
490 if carbon_copy:
491 print("Will also send CCs to: " + ", ".join(carbon_copy))
492 if Options["Do-Close"]: 492 ↛ 493line 492 didn't jump to line 493 because the condition on line 492 was never true
493 print("Will also close associated bug reports.")
494 print()
495 print("------------------- Reason -------------------")
496 print(Options["Reason"])
497 print("----------------------------------------------")
498 print()
500 if Options["Rdep-Check"]:
501 arches = utils.split_args(Options["Architecture"])
502 include_arch_all = Options["NoArchAllRdeps"] == ""
503 check_all_arches = include_arch_all and "all" in arches
504 # when arches is None, rdeps are checked on all arches in the suite
505 reverse_depends_check(
506 removals,
507 suites[0],
508 arches if not check_all_arches else None,
509 session,
510 include_arch_all=include_arch_all,
511 )
513 # If -n/--no-action, drop out here
514 if Options["No-Action"]:
515 sys.exit(0)
517 print("Going to remove the packages now.")
518 game_over()
520 # Do the actual deletion
521 print("Deleting...", end=" ")
522 sys.stdout.flush()
524 bugs = utils.split_args(Options["Done"])
525 remove(
526 session,
527 Options["Reason"],
528 suites,
529 [j[0:4] for j in to_remove],
530 partial=bool(Options["Partial"]),
531 components=utils.split_args(Options["Component"]),
532 done_bugs=bugs,
533 carbon_copy=carbon_copy,
534 close_related_bugs=bool(Options["Do-Close"]),
535 )
537 print("done.")